AgentSkillsCN

shaping

当与用户协作制定解决方案时使用此方法——迭代定义问题(需求)和解决方案选项(形态)。包括面包板测试以映射可用性和连线,以及按垂直实施增量切分。

SKILL.md
--- frontmatter
name: shaping
description: Use this methodology when collaboratively shaping a solution with the user - iterating on problem definition (requirements) and solution options (shapes). Includes breadboarding to map affordances and wiring, and slicing into vertical implementation increments.

Shaping Methodology

A structured approach for collaboratively defining problems, exploring solution options, detailing them into concrete affordances via breadboarding, and slicing into vertical implementation increments.


Multi-Level Consistency (Critical)

Shaping produces documents at different levels of abstraction. Truth must stay consistent across all levels.

The Document Hierarchy (high to low)

  1. Shaping doc — ground truth for R's, shapes, parts, fit checks
  2. Slices doc — ground truth for slice definitions, breadboards
  3. Individual slice plans (V1-plan, etc.) — ground truth for implementation details

The Principle

Each level summarizes or provides a view into the level(s) below it. Lower levels contain more detail; higher levels are designed views that help acquire context quickly.

Changes ripple in both directions:

  • Change at high level → trickles down: If you change the shaping doc's parts table, update the slices doc too.
  • Change at low level → trickles up: If a slice plan reveals a new mechanism or changes the scope of a slice, the Slices doc and shaping doc must reflect that.

The Practice

Whenever making a change:

  1. Identify which level you're touching
  2. Ask: "Does this affect documents above or below?"
  3. Update all affected levels in the same operation
  4. Never let documents drift out of sync

The system only works if the levels are consistent with each other.


Starting a Session

When kicking off a new shaping session, offer the user both entry points:

  • Start from R (Requirements) — Describe the problem, pain points, or constraints. Build up requirements and let shapes emerge.
  • Start from S (Shapes) — Sketch a solution already in mind. Capture it as a shape and extract requirements as you go.

There is no required order. Shaping is iterative — R and S inform each other throughout.

Working with an Existing Shaping Doc

When the shaping doc already has a selected shape:

  1. Display the fit check for the selected shape only — Show R x [selected shape] (e.g., R x F), not all shapes
  2. Summarize what is unsolved — Call out any requirements that are Undecided, or where the selected shape has a fail

This gives the user immediate context on where the shaping stands and what needs attention.


Core Concepts

R: Requirements

A numbered set defining the problem space.

  • R0, R1, R2... are members of the requirements set
  • Requirements are negotiated collaboratively - not filled in automatically
  • Track status: Core goal, Undecided, Leaning yes/no, Must-have, Nice-to-have, Out
  • Requirements extracted from fit checks should be made standalone (not dependent on any specific shape)
  • R states what's needed, not what's satisfied — satisfaction is always shown in a fit check (R x S)
  • Chunking policy: Never have more than 9 top-level requirements. When R exceeds 9, group related requirements into chunks with sub-requirements (R3.1, R3.2, etc.) so the top level stays at 9 or fewer. This keeps the requirements scannable and forces meaningful grouping.

S: Shapes (Solution Options)

Letters represent mutually exclusive solution approaches.

  • A, B, C... are top-level shape options (you pick one)
  • C1, C2, C3... are components/parts of Shape C (they combine)
  • C3-A, C3-B, C3-C... are alternative approaches to component C3 (you pick one)

Shape Titles

Give shapes a short descriptive title that characterizes the approach. Display the title when showing the shape:

markdown
## E: Modify CUR in place to follow S-CUR

| Part | Mechanism |
|------|-----------|
| E1 | ... |

Good titles capture the essence of the approach in a few words:

  • "E: Modify CUR in place to follow S-CUR"
  • "C: Two data sources with hybrid pagination"

Bad titles:

  • "E: The solution" (too vague)
  • "E: Add search to widget-grid by swapping..." (too long)

Notation Hierarchy

LevelNotationMeaningRelationship
RequirementsR0, R1, R2...Problem constraintsMembers of set R
ShapesA, B, C...Solution optionsPick one from S
ComponentsC1, C2, C3...Parts of a shapeCombine within shape
AlternativesC3-A, C3-B...Approaches to a componentPick one per component

Notation Persistence

Keep notation throughout as an audit trail. When finalizing, compose new options by referencing prior components (e.g., "Shape E = C1 + C2 + C3-A").

Phases

Shaping moves through three phases:

code
Shaping → Breadboarding → Slicing
PhasePurposeOutput
ShapingExplore the problem and solution space, select and detail a shapeShaping doc with R, shapes, fit checks
BreadboardingMap selected shape into concrete affordances and wiringAffordance tables (UI, Code, Data stores), optional Mermaid diagram
SlicingBreak breadboarded shape into vertical implementation incrementsVertical slices with demo-able UI

Phase Transitions

Shaping → Breadboarding happens when:

  • A shape is selected (passes fit check, feels right)
  • The shape has been detailed into parts/mechanisms
  • We need to understand the concrete affordances and wiring

Breadboarding → Slicing happens when:

  • The breadboard is complete (affordance tables, wiring verified)
  • We need to plan implementation order

You can't slice without a breadboard. You can't breadboard without a selected shape.


Fit Check (Decision Matrix)

THE fit check is the single table comparing all shapes against all requirements. Requirements are rows, shapes are columns. This is how we decide which shape to pursue.

Format

markdown
## Fit Check

| Req | Requirement | Status | A | B | C |
|-----|-------------|--------|---|---|---|
| R0 | Make items searchable from index page | Core goal | pass | pass | pass |
| R1 | State survives page refresh | Must-have | pass | fail | pass |
| R2 | Back button restores state | Must-have | fail | pass | pass |

**Notes:**
- A fails R2: [brief explanation]
- B fails R1: [brief explanation]

Conventions

  • Always show full requirement text — never abbreviate or summarize requirements in fit checks
  • Fit check is BINARY — Use pass for pass, fail for fail. No other values.
  • Shape columns contain only pass or fail — no inline commentary; explanations go in Notes section
  • Never use warning symbols in fit check — warnings belong only in the Parts table's flagged column
  • Keep notes minimal — just explain failures

Comparing Alternatives Within a Component

When comparing alternatives for a specific component (e.g., C3-A vs C3-B), use the same format but scoped to that component:

markdown
## C3: Component Name

| Req | Requirement | Status | C3-A | C3-B |
|-----|-------------|--------|------|------|
| R1 | State survives page refresh | Must-have | pass | fail |
| R2 | Back button restores state | Must-have | pass | pass |

Missing Requirements

If a shape passes all checks but still feels wrong, there's a missing requirement. Articulate the implicit constraint as a new R, then re-run the fit check.

Macro Fit Check

A separate tool from the standard fit check, used when working at a high level with chunked requirements and early-stage shapes where most mechanisms are still flagged. Use when explicitly requested.

The macro fit check has two columns per shape instead of one:

  • Addressed? — Does some part of the shape seem to speak to this requirement at a high level?
  • Answered? — Can you trace the concrete how? Is the mechanism actually spelled out?

Format:

markdown
## Macro Fit Check: R x A

| Req | Requirement | Addressed? | Answered? |
|-----|-------------|:----------:|:---------:|
| R0 | Core goal description | yes | no |
| R1 | Guided workflow | yes | no |
| R2 | Agent boundary | partial | no |

Conventions:

  • Only show top-level requirements (R0, R1, R2...), not sub-requirements
  • No notes column — keep the table narrow and scannable
  • Use yes (yes), partial (partially), no (no) for Addressed
  • Use yes (yes) or no (no) for Answered
  • Follow the macro fit check with a separate Gaps table listing specific missing parts and their related sub-requirements

Possible Actions

These can happen in any order:

  • Populate R - Gather requirements as they emerge
  • Sketch a shape - Propose a high-level approach (A, B, C...)
  • Detail (components) - Break a shape into components (B1, B2...)
  • Detail (affordances) - Expand a selected shape into concrete UI/Non-UI affordances and wiring
  • Explore alternatives - For a component, identify options (C3-A, C3-B...)
  • Check fit - Build a fit check (decision matrix) playing options against R
  • Extract Rs - When fit checks reveal implicit requirements, add them to R as standalone items
  • Breadboard - Map the system into affordance tables to understand where changes happen and make the shape concrete
  • Spike - Investigate unknowns to identify concrete steps needed
  • Decide - Pick alternatives, compose final solution
  • Slice - Break a breadboarded shape into vertical slices for implementation

Communication

Show Full Tables

When displaying R (requirements) or any S (shapes), always show every row — never summarize or abbreviate. The full table is the artifact; partial views lose information and break the collaborative process.

  • Show all requirements, even if many
  • Show all shape parts, including sub-parts (E1.1, E1.2...)
  • Show all alternatives in fit checks

Why This Matters

Shaping is collaborative negotiation. The user needs to see the complete picture to:

  • Spot missing requirements
  • Notice inconsistencies
  • Make informed decisions
  • Track what's been decided

Summaries hide detail and shift control away from the user.

Mark Changes

When re-rendering a requirements table or shape table after making changes, mark every changed or added line so the user can instantly spot what's different. Place the marker at the start of the changed cell content. This makes iterative refinement easy to follow — the user should never have to diff the table mentally.

Spikes

A spike is an investigation task to learn how the existing system works and what concrete steps are needed to implement a component. Use spikes when there's uncertainty about mechanics or feasibility.

File Management

Always create spikes in their own file (e.g., spike.md or spike-[topic].md). Spikes are standalone investigation documents that may be shared or worked on independently from the shaping doc.

Purpose

  • Learn how the existing system works in the relevant area
  • Identify what we would need to do to achieve a result
  • Enable informed decisions about whether to proceed
  • Not about effort — effort is implicit in the steps themselves
  • Investigate before proposing — discover what already exists; you may find the system already satisfies requirements

Structure

markdown
## [Component] Spike: [Title]

### Context
Why we need this investigation. What problem we're solving.

### Goal
What we're trying to learn or identify.

### Questions

| # | Question |
|---|----------|
| **X1-Q1** | Specific question about mechanics |
| **X1-Q2** | Another specific question |

### Acceptance
Spike is complete when all questions are answered and we can describe [the understanding we'll have].

Acceptance Guidelines

Acceptance describes the information/understanding we'll have, not a conclusion or decision:

  • Good: "...we can describe how users set their language and where non-English titles appear"
  • Good: "...we can describe the steps to implement [component]"
  • Bad: "...we can answer whether this is a blocker" (that's a decision, not information)
  • Bad: "...we can decide if we should proceed" (decision comes after the spike)

The spike gathers information; decisions are made afterward based on that information.

Question Guidelines

Good spike questions ask about mechanics:

  • "Where is the [X] logic?"
  • "What changes are needed to [achieve Y]?"
  • "How do we [perform Z]?"
  • "Are there constraints that affect [approach]?"

Avoid:

  • Effort estimates ("How long will this take?")
  • Vague questions ("Is this hard?")
  • Yes/no questions that don't reveal mechanics

Shape Parts

Flagged Unknown

A mechanism can be described at a high level without being concretely understood. The Flag column tracks this:

PartMechanismFlag
F1Create widget (component, def, register)
F2Magic authentication handlerflagged
  • Empty = mechanism is understood — we know concretely how to build it
  • Flagged = flagged unknown — we've described WHAT but don't yet know HOW

Why flagged unknowns fail the fit check:

  1. Pass is a claim of knowledge — it means "we know how this shape satisfies this requirement"
  2. Satisfaction requires a mechanism — some part that concretely delivers the requirement
  3. A flag means we don't know how — we've described what we want, not how to build it
  4. You can't claim what you don't know — therefore it must be fail

Fit check is always binary — pass or fail only. There is no third state. A flagged unknown is a failure until resolved.

This distinguishes "we have a sketch" from "we actually know how to do this." Early shapes (A, B, C) often have many flagged parts — that's fine for exploration. But a selected shape should have no flags (all fails resolved), or explicit spikes to resolve them.

Parts Must Be Mechanisms

Shape parts describe what we BUILD or CHANGE — not intentions or constraints:

  • Good: "Route childType === 'letter' to typesenseService.rawSearch()" (mechanism)
  • Bad: "Types unchanged" (constraint — belongs in R)

Avoid Tautologies Between R and S

R states the need/constraint (what outcome). S describes the mechanism (how to achieve it). If they say the same thing, the shape part isn't adding information.

  • Bad: R17: "Admins can bulk request members to sign" + C6.3: "Admin can bulk request members to sign"
  • Good: R17: "Admins can bring existing members into waiver tracking" + C6.3: "Bulk request UI with member filters, creates WaiverRequests in batch"

The requirement describes the capability needed. The shape part describes the concrete mechanism that provides it. If you find yourself copying text from R into S, stop — the shape part should add specificity about how.

Parts Should Be Vertical Slices

Avoid horizontal layers like "Data model" that group all tables together. Instead, co-locate data models with the features they support:

  • Bad: B4: Data model — Waivers table, WaiverSignatures table, WaiverRequests table
  • Good: B1: Signing handler — includes WaiverSignatures table + handler logic
  • Good: B5: Request tracking — includes WaiverRequests table + tracking logic

Each part should be a vertical slice containing the mechanism AND the data it needs.

Extract Shared Logic

When the same logic appears in multiple parts, extract it as a standalone part that others reference:

  • Bad: Duplicating "Signing handler: create WaiverSignature + set boolean" in B1 and B2
  • Good: Extract as B1: Signing handler, then B2 and B3 say "calls B1"
markdown
| **B1** | **Signing handler** |
| B1.1 | WaiverSignatures table: memberId, waiverId, signedAt |
| B1.2 | Handler: create WaiverSignature + set member.waiverUpToDate = true |
| **B2** | **Self-serve signing** |
| B2 | Self-serve purchase: click to sign inline -> calls B1 |
| **B3** | **POS signing via email** |
| B3.1 | POS purchase: send waiver email |
| B3.2 | Passwordless link to sign -> calls B1 |

Hierarchical Notation

Start with flat notation (E1, E2, E3...). Only introduce hierarchy (E1.1, E1.2...) when:

  • There are too many parts to easily understand
  • You're reaching a conclusion and want to show structure
  • Grouping related mechanisms aids communication
NotationMeaning
E1Top-level component of shape E
E1.1, E1.2Sub-parts of E1 (add later if needed)

Example of hierarchical grouping (used when shape is mature):

PartMechanism
E1Swap data source
E1.1Modify backend indexer
E1.2Route letters to new service
E1.3Route posts to new service
E2Add search input
E2.1Add input with debounce

Detailing a Shape

When a shape is selected, you can expand it into concrete affordances. This is called detailing.

Notation

Use "Detail X" (not a new letter) to show this is a breakdown of Shape X, not an alternative:

markdown
## A: First approach
(shape table)

## B: Second approach
(shape table)

## Detail B: Concrete affordances
(affordance tables + wiring)

What Detailing Produces

Detailing produces a breadboard:

  • UI Affordances table — Things users see and interact with (inputs, buttons, displays)
  • Non-UI Affordances table — Data stores, handlers, queries, services
  • Wiring diagram — How affordances connect across places

Why "Detail X" Not "C"

Shape letters (A, B, C...) are mutually exclusive alternatives — you pick one. Detailing is not an alternative; it's a deeper breakdown of the selected shape. Using a new letter would incorrectly suggest it's a sibling option.

code
A, B, C = alternatives (pick one)
Detail B = expansion of B (not a choice)

Documents

Shaping produces up to four documents. Each has a distinct role:

DocumentContainsPurpose
FrameSource, Problem, OutcomeThe "why" — concise, stakeholder-level
Shaping docRequirements, Shapes (CURRENT/A/B/...), Affordances, Breadboard, Fit CheckThe working document — exploration and iteration happen here
Slices docSlice details, affordance tables per slice, wiring diagramsThe implementation plan — how to build incrementally
Slice plansV1-plan.md, V2-plan.md, etc.Individual implementation plans for each slice

Document Lifecycle

code
Frame (problem/outcome)
    |
Shaping (explore, detail, breadboard)
    |
Slices (plan implementation)

Frame can be written first — it captures the "why" before any solution work begins. It contains:

  • Source — Original requests, quotes, or material that prompted the work (verbatim)
  • Problem — What's broken, what pain exists (distilled from source)
  • Outcome — What success looks like (high-level, not solution-specific)

Capturing Source Material

When the user provides source material during framing (user requests, quotes, emails, slack messages, etc.), always capture it verbatim in a Source section at the top of the frame document.

markdown
## Source

> I'd like to ask again for your thoughts on a user scenario...
>
> Small reminder: at the moment, if I want to keep my country admin rights
> for Russia and Crimea while having Europe Center as my home center...

> [Additional source material added as received]

---

## Problem
...

Why this matters:

  • The source is the ground truth — Problem/Outcome are interpretations
  • Preserves context that may be relevant later
  • Allows revisiting the original request if the distillation missed something
  • Multiple sources can be added as they arrive during framing

When to capture:

  • User pastes a request or quote
  • User shares an email or message from a stakeholder
  • User describes a scenario they were told about
  • Any raw material that informs the frame

Shaping doc is where active work happens. All exploration, requirements gathering, shape comparison, breadboarding, and fit checking happens here. This is the working document and ground truth for R, shapes, parts, and fit checks.

Slices doc is created when the selected shape is breadboarded and ready to build. It contains the slice breakdown, affordance tables per slice, and detailed wiring.

File Management

  • Shaping doc: Update freely as you iterate — this is the ground truth
  • Slices doc: Created when ready to slice, updated as slice scope clarifies
  • Slice plans: Individual files (V1-plan.md, etc.) with implementation details

Frontmatter

Every shaping document (shaping doc, frame, slices doc) must include shaping: true in its YAML frontmatter. This enables tooling hooks (e.g., ripple-check reminders) that help maintain consistency across documents.

markdown
---
shaping: true
---

# [Feature Name] — Shaping
...

Keeping Documents in Sync

See Multi-Level Consistency at the top of this document. Changes at any level must ripple to affected levels above and below.


Breadboarding

Breadboarding transforms a shape's parts into a complete map of affordances and their relationships. The output is always a set of tables showing numbered UI and Code affordances with their Wires Out and Returns To relationships. The tables are the truth. Mermaid diagrams are optional visualizations for humans.


Breadboarding Use Cases

Breadboarding serves two functions:

1. Mapping an Existing System

You don't understand how an existing system works in its concrete details. You have a workflow you're trying to understand — explaining how something happens or why something doesn't happen.

Input:

  • Code repo(s) to analyze
  • Workflow description (always from the perspective of an operator trying to make an effect happen — through UI or as a caller)

Output:

  • UI Affordances table
  • Code Affordances table
  • (Optional) Mermaid visualization

Note: If the workflow spans multiple applications (frontend + backend), create ONE breadboard that tells the full story. Label places to show which system they belong to.

2. Designing from Shaped Parts

You have a new system sketched as an assembly of parts (mechanisms) per shaping. You need to detail out the concrete mechanism and show how those parts interact as a system.

Input:

  • Parts list (mechanisms from shaping)
  • The R (requirement/outcome) the parts are meant to achieve
  • Existing system (optional) — if the new parts must interoperate with existing code

Output:

  • UI Affordances table
  • Code Affordances table
  • (Optional) Mermaid visualization

Mixtures

Often you have both: an existing system that must remain as-is, plus new pieces or changes defined in a shape. In this case, breadboard both together — the existing affordances and the new ones — showing how they connect.

CURRENT as Reserved Shape Name

Use CURRENT to describe the existing system. This provides a baseline for understanding where proposed changes fit.

Tables Are the Source of Truth

The affordance tables (UI and Non-UI) define the breadboard. The Mermaid diagram renders them.

When receiving feedback on a breadboard:

  1. First — update the affordance tables (add/remove/modify affordances, update Wires Out)
  2. Then — update the Mermaid diagram to reflect those changes

Never treat the diagram as the primary artifact. Changes flow from tables to diagram, not the reverse.

3. Reading a Whiteboard Breadboard

Hand-drawn or whiteboard breadboards use a visual stacking format rather than tables. The same concepts apply (Places, affordances, wiring) but the layout conventions differ.

Visual conventions:

ElementHow it appears
PlaceColored block (often pink/purple) at the top of a vertical stack
Affordances in a placeBlocks stacked underneath the place block — containment is shown by vertical position in the stack
Code affordancesTypically float between place stacks, not inside them
Place loaderA code affordance positioned at the top-left of the place block — describes the data/inputs needed to render that place
Wires OutSolid arrows between blocks
Returns ToDashed arrows between blocks
ConditionalsIndented blocks within a stack, often a different color (e.g., green), showing if/else branches
Place references_ prefix on a place name within a stack (same as _PlaceName convention)
Uncertain/tentative? prefix or ~ prefix on an affordance name, or dashed borders — indicates the affordance is speculative
Containing boxA large boundary drawn around multiple stacks — groups affordances by system or responsibility boundary (e.g., "HireEZ" box)
Notes/annotationsFreeform text near elements — context, open questions, or rationale

How to read a whiteboard breadboard:

  1. Identify places — Find the colored header blocks at the top of each stack
  2. Read each stack top-to-bottom — Everything stacked under a place belongs to that place
  3. Find loaders — Code affordances at the top-left of a place block describe what data is needed to render
  4. Trace wiring — Follow arrows between stacks for control flow (solid) and data flow (dashed)
  5. Note conditionals — Indented blocks with different colors show branching logic within a place
  6. Check containing boxes — Large boundaries indicate system/responsibility boundaries
  7. Flag speculative items? and ~ prefixed items are uncertain and may not survive shaping

Translating to tables: When converting a whiteboard breadboard to standard affordance tables, map each stack to its Place, enumerate the affordances top-to-bottom, and capture the arrows as Wires Out / Returns To relationships. Loaders become code affordances with Returns To pointing at the UI affordances they feed.


Breadboard Concepts

Places

A Place is a bounded context of interaction. While you're in a Place:

  • You have a specific set of affordances available to you
  • You cannot interact with affordances outside that boundary
  • You must take an action to leave

Place is perceptual, not technical. It's not about URLs or components — it's about what the user experiences as their current context. A Place is "where you are" in terms of what you can do right now.

The Blocking Test

The simplest test for whether something is a different Place: Can you interact with what's behind?

AnswerMeaning
NoYou're in a different Place
YesSame Place, with local state changes

Examples

UI ElementBlocking?Place?Why
ModalYesYesCan't interact with page behind
Confirmation popoverYesYesMust respond before returning (limit case of modal)
Edit mode (whole screen transforms)YesYesAll affordances changed
Checkbox reveals extra fieldsNoNoSurroundings unchanged
Dropdown menuNoNoCan click away, non-blocking
TooltipNoNoInformational, non-blocking

Local State vs Place Navigation

When a control changes state, ask: did everything change, or just a subset while the surroundings stayed the same?

TypeWhat happensHow to model
Local stateSubset of UI changes, surroundings unchangedSame Place, conditional N -> dependent Us
Place navigationEntire screen transforms, or blocking overlayDifferent Places

Mode-Based Places

When a mode (like "edit mode") transforms the entire screen — different buttons, different affordances everywhere — model as separate Places:

code
PLACE: CMS Page (Read Mode)
PLACE: CMS Page (Edit Mode)

The state flag (e.g., editMode$) that switches between them is a navigation mechanism, not a data store. Don't include it as an S in either Place.

Three Questions for Any Control

For any UI affordance, ask:

  1. Where did I come from to see this?
  2. Where am I now?
  3. Where do I go if I act on it?

If the answer to #3 is "everything changes" or "I can't interact with what's behind until I respond," that's navigation to a different Place.

Labeling Conventions

PatternUse
PLACE: Page NameStandard page/route
PLACE: Page Name (Mode)Mode-based variant of a page
PLACE: Modal NameModal dialog
PLACE: BackendAPI/database boundary

When spanning multiple systems, label with the system: PLACE: Checkout Page (frontend), PLACE: Payment API (backend).

Place IDs

Places are first-class elements in the data model. Each Place gets an ID:

#PlaceDescription
P1CMS Page (Read Mode)View-only state
P2CMS Page (Edit Mode)Editing state with page-level controls
P2.1widget-grid (letters)Subplace: letter editing widget within P2
P3Letter Form ModalForm for adding/editing letters
P4BackendAPI resolvers and database

Place IDs enable:

  • Explicit navigation wiring — wire -> P2 instead of to an affordance inside
  • Containment tracking — each affordance declares which Place it belongs to
  • Consistent Mermaid subgraphs — subgraph ID matches Place ID

Place References

When a nested place has lots of internal affordances and would clutter the parent, you can detach it:

  1. Put a reference node in the parent place using underscore prefix: _letter-browser
  2. Define the full place separately with all its internals
  3. Wire from the reference to the place: _letter-browser --> letter-browser

The reference is a UI affordance — it represents "this widget/component renders here" in the parent context.

mermaid
flowchart TB
subgraph P1["P1: CMS Page (Read Mode)"]
    U1["U1: Edit button"]
    U_LB["_letter-browser"]
end

subgraph letterBrowser["letter-browser"]
    U10["U10: Search input"]
    U11["U11: Letter list"]
    N40["N40: performSearch()"]
end

U_LB --> letterBrowser

In affordance tables, list the reference as a UI affordance:

#AffordanceControlWires Out
U1Edit buttonclick-> N1
_letter-browserWidget reference-> P3

Style place references with a dashed border to distinguish them:

code
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5
class U_LB placeRef

Modes as Places

When a component has distinct modes (read vs edit, viewing vs editing, collapsed vs expanded), model them as separate places — they're different perceptual states for the user.

If one mode includes everything from another plus more, show this with a place reference inside the extended place:

code
P3: letter-browser (Read)    — base state
P4: letter-browser (Edit)    — contains _letter-browser (Read) + new affordances

The reference shows composition: "everything in P3 appears here, plus these additions."

mermaid
flowchart TB
subgraph P3["P3: letter-browser (Read)"]
    U10["U10: Search input"]
    U11["U11: Letter list"]
end

subgraph P4["P4: letter-browser (Edit)"]
    U_P3["_letter-browser (Read)"]
    U3["U3: Add button"]
    U4["U4: Edit button"]
end

U_P3 --> P3

In affordance tables for P4, the reference shows inheritance:

#AffordanceControlWires OutNotes
_letter-browser (Read)Inherits all of P3-> P3
U3Add buttonclick-> N3NEW
U4Edit buttonclick-> N4NEW

Subplaces

A subplace is a defined subset of a Place — a contained area that groups related affordances. Use subplaces when:

  • A Place contains multiple distinct widgets or sections
  • You're detailing one part of a larger Place
  • You want to show what's in scope vs out of scope

Notation: Use hierarchical IDs — P2.1, P2.2, etc. for subplaces of P2.

code
| # | Place | Description |
|---|-------|-------------|
| P2 | Dashboard | Main dashboard page |
| P2.1 | Sales widget | Subplace: sales metrics |
| P2.2 | Activity feed | Subplace: recent activity |

In affordance tables, use the subplace ID to show containment:

code
| U3 | P2.1 | sales-widget | "Refresh" button | click | -> N4 | — |
| U7 | P2.2 | activity-feed | activity list | render | — | — |

In Mermaid: Nest the subplace subgraph inside the parent. Use the same background color (no distinct fill) — the subplace is part of the parent, not a separate Place:

mermaid
flowchart TB
subgraph P2["P2: Dashboard"]
    subgraph P2_1["P2.1: Sales widget"]
        U3["U3: Refresh button"]
    end
    subgraph P2_2["P2.2: Activity feed"]
        U7["U7: activity list"]
    end
    otherContent[["... other dashboard content ..."]]
end

Placeholder for out-of-scope content: When detailing one subplace, add a placeholder sibling to show there's more on the page:

code
otherContent[["... other page content ..."]]

This tells readers: "we're zooming in on P2.1, but P2 contains more that we're not detailing."

Containment vs Wiring

These are two different relationships in the data model:

RelationshipMeaningWhere Captured
ContainmentAffordance belongs to / lives in a PlacePlace column (set membership)
WiringAffordance triggers / calls somethingWires Out column (control flow)

Containment is set membership: U1 in P1 means U1 is a member of Place P1. Every affordance belongs to exactly one Place.

Wiring is control flow: U1 -> N1 means U1 triggers N1. An affordance can wire to anything — other affordances or Places.

The Place column answers: "Where does this affordance live?" The Wires Out column answers: "What does this affordance trigger?"

Navigation Wiring

When an affordance causes navigation (user "goes" somewhere), wire to the Place itself, not to an affordance inside:

code
Good: N1 Wires Out: -> P2          (navigate to Edit Mode)
Bad:  N1 Wires Out: -> U3          (wiring to affordance inside P2)

This makes navigation explicit in the tables. The Place is the destination; specific affordances inside become available once you arrive.

In Mermaid, this becomes:

code
N1 --> P2

The subgraph ID matches the Place ID, so the wire connects to the Place boundary.

Affordances

Things you can act upon:

  • UI affordances (U): inputs, buttons, displayed elements, scroll regions
  • Code affordances (N): methods, subscriptions, data stores, framework mechanisms

Wiring

How affordances connect to each other:

Wires Out — What an affordance triggers or calls (control flow):

  • Call wires: one affordance calls another
  • Write wires: code writes to a data store
  • Navigation wires: routing to a different place

Returns To — Where an affordance's output flows (data flow):

  • Return wires: function returns value to its caller
  • Read wires: data store is read by another affordance

This separation makes data flow explicit. Wires Out show control flow (what triggers what). Returns To show data flow (where output goes).


Affordance Tables

The tables are the truth. Every breadboard produces these:

Places Table

#PlaceDescription
P1Search PageMain search interface
P2Detail PageIndividual result view

UI Affordances Table

#PlaceComponentAffordanceControlWires OutReturns To
U1P1search-detailsearch inputtype-> N1
U2P1search-detailloading spinnerrender
U3P1search-detailresults listrender
U4P1search-detailresult rowclick-> P2

Code Affordances Table

#PlaceComponentAffordanceControlWires OutReturns To
N1P1search-detailactiveQuery.next()call-> N2
N2P1search-detailactiveQuery subscriptionobserve-> N3
N3P1search-detailperformSearch()call-> N4, -> N5, -> N6
N4P1search.servicesearchOneCategory()call-> N7-> N3
N5P1search-detailloadingwritestore-> U2
N6P1search-detailresultswritestore-> U3
N7P1typesense.servicerawSearch()call-> N4

Data Stores Table

#PlaceStoreDescription
S1P1resultsArray of search results
S2P1loadingBoolean loading state

Column Definitions

ColumnDescription
#Unique ID (P1, P2... for Places; U1, U2... for UI; N1, N2... for Code; S1, S2... for Stores)
PlaceWhich Place this affordance belongs to (containment)
ComponentWhich component/service owns this
AffordanceThe specific thing you can act upon
ControlThe triggering event: click, type, call, observe, write, render
Wires OutWhat this triggers: -> N4, -> P2 (control flow, including navigation)
Returns ToWhere output flows: -> N3 or -> U2, U3 (data flow)

Breadboarding Procedures

For Mapping an Existing System

See Example A below for a complete worked example.

Step 1: Identify the flow to analyze

Pick a specific user journey. Always frame it as an operator trying to do something:

  • "Land on /search, type query, scroll for more, click result"
  • "Call the payment API with a card token, expect a charge to be created"

Step 2: List all places involved

Walk through the journey and identify each distinct place the user visits or system boundary crossed.

Step 3: Trace through the code to find components

Starting from the entry point (route, API endpoint), trace through the code to find every component touched by that flow.

Step 4: For each component, list its affordances

Read the code. Identify:

  • UI: What can the user see and interact with?
  • Code: What methods, subscriptions, stores are involved?

Step 5: Name the actual thing, not an abstraction

If you write "DATABASE", stop. What's the actual method? (userRepo.save()). Every affordance name must be something real you can point to in the code.

Step 6: Fill in Control column

For each affordance, what triggers it? (click, type, call, observe, write, render)

Step 7: Fill in Wires Out

For each affordance, what does it trigger? Read the code — what does this method call? What does this button's handler invoke?

Step 8: Fill in Returns To

For each affordance, where does its output flow?

  • Functions that return values -> list the callers that receive the return
  • Data stores -> list the affordances that read from them
  • No meaningful output -> use

Step 9: Add data stores as affordances

When code writes to a property that is later read by another affordance, add that property as a Code affordance with control type write.

Step 10: Add framework mechanisms as affordances

Include things like cdr.detectChanges() that bridge between code and UI rendering. These show how state changes actually reach the UI.

Step 11: Verify against the code

Read the code again. Confirm every affordance exists and the wiring matches reality.


For Designing from Shaped Parts

See Example B below for a complete worked example including slicing.

Step 1: List each part from the shape

Take each mechanism/part identified in shaping and write it down.

Step 2: Translate parts into affordances

For each part, identify:

  • What UI affordances does this part require?
  • What Code affordances implement this part?

Step 3: Verify every U has a supporting N

For each UI affordance, check: what Code affordance provides its data or controls its rendering? If none exists, add the missing N.

Step 4: Classify places as existing or new

For each UI affordance, determine whether it lives in:

  • An existing place being modified
  • A new place being created

Step 5: Wire the affordances

Fill in Wires Out and Returns To for each affordance. Trace through the intended behavior — what calls what? What returns where?

Step 6: Connect to existing system (if applicable)

If there's an existing codebase:

  • Identify the existing affordances the new ones must connect to
  • Add those existing affordances to your tables
  • Wire the new affordances to them

Step 7: Check for completeness

  • Every U should have an N that feeds it
  • Every N should have either Wires Out or Returns To (or both)
  • Handlers -> should have Wires Out
  • Queries -> should have Returns To
  • Data stores -> should have Returns To

Step 8: Treat user-visible outputs as Us

Anything the user sees (including emails, notifications) is a UI affordance and needs an N wiring to it.


Breadboard Key Principles

Never use memory — always check the data

When tracing a flow backwards, don't follow the path you remember. Scan the Wires Out column for ALL affordances that wire to your target.

When filling in the tables, read each row systematically. Don't rely on what you think you know.

The tables are the source of truth. Your memory is unreliable.

Every affordance name must exist (when mapping)

When mapping existing code, never invent abstractions. Every name must point to something real in the codebase.

Mechanisms aren't affordances

An affordance is something you can act upon that has meaningful identity in the system. Several things look like affordances but are actually just implementation mechanisms:

TypeExampleWhy it's not an affordance
Visual containersmodal-frame wrapperYou can't act on a wrapper — it's just a Place boundary
Internal transformsletterDataTransform()Implementation detail of the caller — not separately actionable
Navigation mechanismsmodalService.open()Just the "how" of getting to a Place — wire to the destination directly

These aren't always obvious on first draft. When reviewing your affordance tables, double-check each Code affordance and ask:

"Is this actually an affordance, or is it just detailing the mechanism for how something happens?"

If it's just the "how" — skip it and wire directly to the destination or outcome.

Examples:

code
Bad:  N8 --> N22 --> P3     (N22 is modalService.open — just mechanism)
Good: N8 --> P3             (handler navigates to modal)

Bad:  N6 --> N20 --> S2     (N20 is data transform — internal to N6)
Good: N6 --> S2             (callback writes to store)

Bad:  U7: modal-frame       (wrapper — just the boundary of P3)
Good: U8: Save button       (actionable)

The handler navigates to P3. The callback writes to the store. The modal IS P3. The mechanisms are implicit.

Two flows: Navigation and Data

A breadboard captures two distinct flows:

FlowWhat it tracksWiring
NavigationMovement from Place to PlaceWires Out -> Places
DataHow state populates displaysReturns To -> Us

These are orthogonal. You can have navigation without data changes, and data changes without navigation.

When reviewing a breadboard, trace both flows:

  1. Navigation flow: Can you follow the user's journey from Place to Place?
  2. Data flow: For every U that displays data, can you trace where that data comes from?

Every U that displays data needs a source

A UI affordance that displays data must have something feeding it — either a data store (S) or a code affordance (N) that returns data.

code
Bad:  U6: letter list (no incoming wire — where does the data come from?)
Good: S1 -.-> U6 (store feeds the display)
Good: N4 -.-> U6 (query result feeds the display)

If a display U has no data source wiring into it, either:

  1. The source is missing from the breadboard
  2. The U isn't real

This is easy to miss when focused on navigation. Always ask: "This U shows data — where does that data come from?"

Every N must connect

If a Code affordance has no Wires Out AND no Returns To, something is wrong:

  • Handlers -> should have Wires Out (what they call or write)
  • Queries -> should have Returns To (who receives their return value)
  • Data stores -> should have Returns To (which affordances read them)

Side effects need stores

An N that appears to wire nowhere is suspicious. If it has side effects outside the system boundary (browser URL, localStorage, external API, analytics), add a store node to represent that external state:

code
Bad:  N41: updateUrl()           (wires to... nothing?)
Good: N41: updateUrl() -> S15     (wires to Browser URL store)

This makes the data flow explicit. The store can also have return wires showing how external state flows back in:

mermaid
flowchart TB
N42["N42: performSearch()"] --> N41["N41: updateUrl()"]
N41 --> S15["S15: Browser URL (?q=)"]
S15 -.->|back button / init| N40["N40: activeQuery$"]

Common external stores to model:

  • Browser URL — query params, hash fragments
  • localStorage / sessionStorage — persisted client state
  • Clipboard — copy/paste operations
  • Browser History — navigation state

Separate control flow from data flow

Wires Out = control flow (what triggers what) Returns To = data flow (where output goes)

This separation makes the system's behavior explicit.

Show navigation inline, not as loops

Routing is a generic mechanism every page uses. Instead of drawing all navigation through a central Router affordance, show Router navigate() inline where it happens and wire directly to the destination place.

Place stores where they enable behavior, not where they're written

A data store belongs in the Place where its data is consumed to enable some effect — not where it's produced. Writes from other Places are "reaching into" that Place's state.

To determine where a store belongs:

  1. Trace read/write relationships — Who writes? Who reads?
  2. The readers determine placement — that's where behavior is enabled
  3. If only one Place reads, the store goes inside that Place

Example: A changedPosts array is written by a Modal (when user confirms changes) but read by a PAGE_SAVE handler (when user clicks Save). The store belongs with the PAGE_SAVE handler — that's where it enables the persistence operation.

Only extract to shared areas when truly shared

Before putting a store in a separate DATA STORES section, verify it's actually read by multiple Places. If it only enables behavior in one Place, it belongs inside that Place.

Nest stores in the subcomponent that reads them

Within a Place, put stores in the subcomponent where they enable behavior. If a store is read by a specific handler, put it in that handler's component — not floating at the Place level.

Backend is a Place

The database and resolvers aren't floating infrastructure — they're a Place with their own affordances. Database tables (S) belong inside the Backend Place alongside the resolvers (N) that read and write them.


Catalog of Parts and Relationships

This section provides a complete reference of everything that can appear in a breadboard.

Elements

ElementID PatternWhat It IsWhat Qualifies
PlaceP1, P2, P3...A bounded context of interactionBlocking test: can't interact with what's behind
SubplaceP2.1, P2.2...A defined subset within a PlaceGroups related affordances within a larger Place
Place Reference_PlaceNameUI affordance pointing to a detached placeComplex nested place defined separately
UI AffordanceU1, U2, U3...Something the user can see or interact withInputs, buttons, displays, scroll regions
Code AffordanceN1, N2, N3...Something in code you can act uponMethods, subscriptions, handlers, framework mechanisms
Data StoreS1, S2, S3...State that persists and is read/writtenProperties, arrays, observables that hold data
ChunkA collapsed subsystemOne wire in, one wire out, many internals
PlaceholderOut-of-scope content markerShows context without detailing

Relationships

RelationshipSyntaxMeaningExample
ContainmentPlace columnAffordance belongs to PlaceU3 in Place P2.1
Wires Out-> XControl flow: triggers/calls-> N4, -> P2
Returns To-> X (in Returns To column)Data flow: output goes to-> U6, -> N3
Abbreviated flow|label|Intermediate steps omittedS4 -.-> |view query| U6
Parent-childHierarchical IDSubplace belongs to PlaceP2.1 is child of P2

What Qualifies as Each Element

Place (P):

  • Passes the blocking test — can't interact with what's behind
  • Examples: modal, edit mode (whole screen transforms), route/page
  • Not: dropdown, tooltip, checkbox revealing fields

Place Reference (_PlaceName):

  • A UI affordance that represents a detached place
  • Use when a nested place has many affordances and would clutter the parent
  • Examples: _letter-browser, _user-profile-widget
  • Wires to the full place definition: _letter-browser --> P3

UI Affordance (U):

  • User can see it or interact with it
  • Examples: button, input, list, spinner, displayed text
  • Not: wrapper elements, layout containers

Code Affordance (N):

  • Has meaningful identity — you can point to it in code
  • Examples: handleSubmit(), query$ subscription, detectChanges()
  • Not: internal transforms, navigation mechanisms (see above)

Data Store (S):

  • State that is written and read
  • Examples: results array, loading boolean, changedPosts list
  • External stores: Browser URL, localStorage, Clipboard — represent state outside the app boundary
  • Not: config that's set once and never changes (consider as config affordance)

Verification Checks

CheckQuestionIf No...
Every U that displays dataDoes it have an incoming wire (via Wires Out or Returns To)?Add the data source
Every NDoes it have Wires Out or Returns To (or both)?Investigate — may be dead code or missing wiring
Every SDoes something read from it (Returns To)?Investigate — may be unused
Navigation mechanismsIs this N just the "how" of getting somewhere?Wire directly to Place instead
N with side effectsDoes this N affect external state (URL, storage, clipboard)?Add a store for the external state

Chunking

Chunking collapses a subsystem into a single node in the main diagram, with details shown separately. Use chunking to manage complexity when a section of the breadboard has:

  • One wire in (single entry point)
  • One wire out (single output)
  • Lots of internals between them

When to Chunk

Look for sections where tracing the wiring reveals a "pinch point" — many affordances that funnel through a single input and single output. These are natural boundaries for chunking.

Example: A dynamic-form component receives a form definition, renders many fields (U7a-U7k), validates on change (N26), and emits a single valid$ signal. In the main diagram, this becomes:

code
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8

How to Chunk

  1. In the main diagram, replace the subsystem with a single stadium-shaped node:
code
dynamicForm[["CHUNK: dynamic-form"]]
  1. Wire to/from the chunk using the boundary signals:
code
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8
  1. Create a separate chunk diagram showing the internals with boundary markers:
mermaid
flowchart TB
    input([formDefinition])
    output(["valid$"])

    subgraph chunk["dynamic-form internals"]
        N25["N25: generateFormConfig()"]
        U7a["U7a: field"]
        N26["N26: form value changes"]
        N27["N27: valid$ emission"]
    end

    input --> N25
    N25 --> U7a
    U7a --> N26
    N26 --> N27
    N27 --> output

    classDef boundary fill:#b3e5fc,stroke:#0288d1,stroke-dasharray:5 5
    class input,output boundary
  1. Style chunks distinctly in the main diagram:
code
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
class dynamicForm chunk

Chunk Color Convention

TypeColorHex
Chunk node (main diagram)Light blue#b3e5fc
Boundary markers (chunk diagram)Light blue, dashed#b3e5fc with stroke-dasharray:5 5

Benefits

  • Main diagram stays readable — complex subsystems become single nodes
  • Detail preserved — chunk diagrams show the internals when needed
  • Natural boundaries — chunks often map to reusable components

Visualization (Mermaid)

The tables are the truth. Mermaid diagrams are optional visualizations for humans.

Basic Structure

mermaid
flowchart TB
    U1["U1: search input"] --> N1["N1: activeQuery.next()"]
    N1 --> N2["N2: subscription"]
    N2 --> N3["N3: performSearch"]
    N3 --> N4["N4: searchOneCategory"]
    N4 -.-> N3
    N3 --> N5["N5: loading store"]
    N3 --> N6["N6: results store"]
    N5 -.-> U2["U2: loading spinner"]
    N6 -.-> U3["U3: results list"]

    classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
    classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
    class U1,U2,U3 ui
    class N1,N2,N3,N4,N5,N6 nonui

Line Conventions

Line StyleMermaid SyntaxUse
Solid (-->)A --> BWires Out: calls, triggers, writes
Dashed (-.->)A -.-> BReturns To: return values, data store reads
Labeled ...`A -.->...

Abbreviating Out-of-Scope Flows

When a data flow has intermediate steps that aren't relevant to the breadboard's scope, abbreviate by wiring directly from source to destination with a ... label:

code
S4 -.->|...| U6

This says "data flows from S4 to U6, with intermediate steps omitted." Use this when:

  • The flow exists but its internals are out of scope
  • You need to show where data originates without detailing the query chain
  • The breadboard focuses on one workflow (e.g., editing) but needs to acknowledge another (e.g., viewing)

ID Prefixes

PrefixTypeExample
PPlacesP1, P2, P3
UUI affordancesU1, U2, U3
NCode affordancesN1, N2, N3
SData storesS1, S2, S3

Color Conventions

TypeColorHex
Places (subgraphs)White/transparent
UI affordancesPink#ffb6c1
Code affordancesGrey#d3d3d3
Data storesLavender#e6e6fa
ChunksLight blue#b3e5fc
Place referencesPink, dashed border#ffb6c1
code
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
classDef store fill:#e6e6fa,stroke:#9370db,color:#000
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5

Subgraph Labels and Place IDs

Use the Place ID as the subgraph ID so navigation wiring connects properly:

mermaid
flowchart TB
subgraph P1["P1: CMS Page (Read Mode)"]
    U1["U1: Edit button"]
    N1["N1: toggleEditMode()"]
end

subgraph P2["P2: CMS Page (Edit Mode)"]
    U2["U2: Save button"]
    U3["U3: Add button"]
end

%% Navigation wires to Place ID
N1 --> P2
TypeID PatternLabel PatternPurpose
PlaceP1, P2...P1: Page NameA bounded context the user visits
TriggerTRIGGER: NameAn event that kicks off a flow (not navigable)
ComponentCOMPONENT: NameReusable UI+logic that appears in multiple places
SystemSYSTEM: NameWhen spanning multiple applications

Key point: The subgraph ID (P1, P2) must match the Place ID from the Places table. This allows navigation wires like N1 --> P2 to connect to the Place boundary.

When spanning multiple systems

mermaid
flowchart TB
    subgraph frontend["SYSTEM: Frontend"]
        U1["U1: submit button"]
        N1["N1: handleSubmit()"]
    end

    subgraph backend["SYSTEM: Backend API"]
        N10["N10: POST /orders"]
        N11["N11: orderService.create()"]
    end

    U1 --> N1
    N1 --> N10
    N10 --> N11

Workflow Step Annotations (Optional)

When breadboarding a specific workflow, you can optionally add numbered step markers to help readers follow the sequence visually. This is useful when:

  • The diagram is complex and the workflow path isn't obvious
  • You want to guide someone through a specific user journey
  • The breadboard will be used as a walkthrough or teaching tool

Format:

Add a Workflow Guide table before the diagram:

markdown
| Step | Action | Where to look |
|------|--------|---------------|
| **1** | Click "Edit" button | U1 -> N1 -> S1 |
| **2** | Edit mode activates | S1 -> N2 -> U3 |
| **3** | Click "Add" | U3 -> N3 -> N8 |

Add step marker nodes in the Mermaid diagram using stadium-shaped nodes:

mermaid
flowchart TB
    %% Step markers
    step1(["1 - CLICK EDIT"])
    step2(["2 - EDIT MODE ON"])
    step3(["3 - CLICK ADD"])

    %% Connect steps to relevant affordances with dashed lines
    step1 -.-> U1
    step2 -.-> N2
    step3 -.-> U3

    %% Style step markers green
    classDef step fill:#90EE90,stroke:#228B22,color:#000,font-weight:bold
    class step1,step2,step3 step

Formatting notes:

  • Use "1 - ACTION" format (number, space, hyphen, space, action)
  • Avoid "1. ACTION" — the period triggers Mermaid's markdown list parser
  • Avoid "1) ACTION" — parentheses can also cause parsing issues
  • Connect step markers to affordances with dashed lines (-.->)
  • Style steps green to distinguish from UI (pink) and Code (grey) affordances

Slicing

After a shape is breadboarded, slice it into vertical implementation increments.

The flow:

  1. Parts -> high-level mechanisms in the shape
  2. Breadboard -> concrete affordances with wiring
  3. Slices -> vertical increments that can each be demoed

Key principle: Every slice must end in demo-able UI. A slice without visible output is a horizontal layer, not a vertical slice.

Document outputs:

  • Slices doc — slice definitions, per-slice affordance tables, sliced breadboard
  • Slice plans — individual implementation plans (V1-plan.md, V2-plan.md, etc.)

What is a Vertical Slice?

A vertical slice is a group of UI and Code affordances that does something demo-able. It cuts through all layers (UI, logic, data) to deliver a working increment.

The opposite is a horizontal slice — doing work on one layer (e.g., "set up all the data models") that isn't clickable from the interface.

The Key Constraint

Every slice must have visible UI that can be demoed. A slice without UI is a horizontal layer, not a vertical slice.

  • Good: "Self-serve Signing Path" (demo: checkout -> sign -> see signature)
  • Bad: "Database Schema" (no demo possible)

Demo-able means:

  • Has an entry point (UI interaction or trigger)
  • Has an observable output (UI renders, effect occurs)
  • Shows meaningful progress toward the R

The shape guides what counts as "meaningful progress" — you're not just grouping affordances arbitrarily, you're grouping them to demonstrate mechanisms working.

Slice Size

  • Too small: Only 1-2 UI affordances, no meaningful demo -> merge with related slice
  • Too big: 15+ affordances or multiple unrelated journeys -> split
  • Right size: A coherent journey with a clear "watch me do this" demo

Aim for max 9 slices. If you need more, the shape may be too large for one cycle.

Wires to Future Slices

A slice may contain affordances with Wires Out pointing to affordances in later slices. These wires exist in the breadboard but aren't implemented yet — they're stubs or no-ops until that later slice is built.

This is normal. The breadboard shows the complete system; slicing shows the order of implementation.

Slicing Procedure

Step 1: Identify the minimal demo-able increment

Look at your breadboard and shape. Ask: "What's the smallest subset that demonstrates the core mechanism working?"

Usually this is:

  • The core data fetch
  • Basic rendering
  • No search, no pagination, no state persistence yet

This becomes V1.

Step 2: Layer additional capabilities as slices

Look at the mechanisms in your shape. Each slice should demonstrate a mechanism working:

  • V2: Search input (demonstrates the search mechanism)
  • V3: Pagination/infinite scroll (demonstrates the pagination mechanism)
  • V4: URL state persistence (demonstrates the state preservation mechanism)
  • etc.

Max 9 slices. If you have more, combine related mechanisms. Features that don't make sense alone should be in the same slice.

Step 3: Assign affordances to slices

Go through every affordance and assign it to the slice where it's first needed to demo that slice's mechanism:

SliceMechanismAffordances
V1Core displayU2, U3, N3, N4, N5, N6, N7
V2SearchU1, N1, N2
V3PaginationU10, N11, N12, N13

Some affordances may have Wires Out to later slices — that's fine. They're implemented in their assigned slice; the wires just don't do anything yet.

Step 4: Create per-slice affordance tables

For each slice, extract just the affordances being added:

V2: Search Works

#ComponentAffordanceControlWires OutReturns To
U1search-detailsearch inputtype-> N1
N1search-detailactiveQuery.next()call-> N2
N2search-detailactiveQuery subscriptionobserve-> N3

Step 5: Write a demo statement for each slice

Each slice needs a concrete demo that shows its mechanism working toward the R:

  • V1: "Widget shows real data from the API"
  • V2: "Type 'dharma', results filter live"
  • V3: "Scroll down, more items load"

The demo should be something you can show a stakeholder that demonstrates progress.

Visualizing Slices in Mermaid

Show the complete breadboard in every slice diagram, but use styling to distinguish scope:

CategoryStyleDescription
This sliceBright colorAffordances being added
Already builtSolid greyPrevious slices
FutureTransparent, dashed borderNot yet built
mermaid
flowchart TB
    U1["U1: search input"]
    U2["U2: loading spinner"]
    N1["N1: activeQuery.next()"]
    N2["N2: subscription"]
    N3["N3: performSearch"]

    U1 --> N1
    N1 --> N2
    N2 --> N3
    N3 --> U2

    %% V2 scope (this slice) = green
    classDef thisSlice fill:#90EE90,stroke:#228B22,color:#000
    %% Already built (V1) = grey
    classDef built fill:#d3d3d3,stroke:#808080,color:#000
    %% Future = transparent dashed
    classDef future fill:none,stroke:#ddd,color:#bbb,stroke-dasharray:3 3

    class U1,N1,N2 thisSlice
    class U2,N3 built

This lets stakeholders see:

  • What's being built now (highlighted)
  • What already exists (grey)
  • What's coming later (faded)

Slice Summary Format

#SliceMechanismDemo
V1Widget with real dataF1, F4, F6"Widget shows letters from API"
V2Search worksF3"Type to filter results"
V3Infinite scrollF5"Scroll down, more load"
V4URL stateF2"Refresh preserves search"

The Mechanism column references parts from the shape, showing which mechanisms each slice demonstrates.



Examples

Example A: Mapping an Existing System

This example shows breadboarding an existing system to understand how data flows through multiple entry points.

Input

Workflow to understand: "How is admin_organisation_countries modified and read downstream? There are multiple entry points: manual edit, checkbox toggle, and batch job."

Output

UI Affordances

#ComponentAffordanceControlWires OutReturns To
U1SSO Adminrole_profiles checkboxesrender
U2SSO Admin"Country Admin" checkboxclicktoggles selection
U3SSO Adminadmin_countries filter_horizontalrender
U4SSO AdminAvailable countries listrender
U5SSO AdminSelected countries listrender
U6SSO AdminAdd / Removeclickmodifies selection
U7SSO AdminSave buttonclick-> N3
U20DWConnect"Country admins" sectionrender
U21(unknown)System email "From" fieldrender

Code Affordances

#ComponentAffordanceControlWires OutReturns To
N1sso/accounts/adminget_fieldsets()call-> U3 (conditional)
N2sso/accounts/modelsget_administrable_user_countries()call-> U4
N3sso/accounts/adminsave_form()call-> N4, -> N5
N4Django AdminForm M2M savecall-> S2
N5sso/forms/mixins_update_user_m2m()call-> S1, -> N6
N6sso/signalsuser_m2m_field_updated signalsignal-> N10
N7CLI/Schedulermanage.py dwbn_cleanupinvoke-> N15
N10sso-dwbn-themedwbn_user_m2m_field_updated()receive-> N11
N11sso-dwbn-themedwbn_user_m2m_field_updated_task()call-> N12
N12sso-dwbn-themeCountry Admin added AND zero admin countries?conditional-> N20
N15sso-dwbn-themeadmin_changes()call-> N16
N16sso-dwbn-themeFor each Country Admin: home center country missing?loop-> N20
N20sso-dwbn-themeGet home center's countrycall-> N21
N21sso-dwbn-themeadmin_organisation_countries.add()call-> S2
N22sso-dwbn-themeupdate_last_modified()call
N30dwconnect2-backendfindCenterAdmins()call-> U20
N31sso/apiget_object_data()call-> external

Data Stores

#StoreDescription
S1role_profilesM2M: which role profiles a user has
S2admin_organisation_countriesM2M: which countries a user administers
S3organisationsUser's home center(s)

Mermaid Diagram

mermaid
flowchart TB
    subgraph stores["DATA STORES"]
        S1["S1: role_profiles"]
        S2["S2: admin_organisation_countries"]
        S3["S3: organisations"]
    end

    subgraph ssoAdmin["PLACE: SSO Admin — User Change Page"]
        subgraph permissions["Permissions fieldset"]
            U1["U1: role_profiles checkboxes"]
            U2["U2: 'Country Admin' checkbox"]
        end

        subgraph userAdmin["User admin fieldset (superuser only)"]
            U3["U3: admin_countries filter_horizontal"]
            U4["U4: Available countries"]
            U5["U5: Selected countries"]
            U6["U6: Add → / Remove ←"]
        end

        U7["U7: Save button"]
        N1["N1: get_fieldsets()"]
        N2["N2: get_administrable_user_countries()"]
        N3["N3: save_form()"]
        N4["N4: Form M2M save"]
        N5["N5: _update_user_m2m()"]
        N6["N6: user_m2m_field_updated signal"]

        N1 -->|is_superuser| userAdmin
        U3 --> U4
        U3 --> U5
        U6 --> U5
        N2 -.-> U4

        U2 --> U7
        U6 --> U7
        U7 --> N3
        N3 --> N4
        N3 --> N5
        N5 --> N6
    end

    subgraph trigger["TRIGGER: Batch Cleanup"]
        N7["N7: manage.py dwbn_cleanup"]
    end

    subgraph theme["sso-dwbn-theme"]
        N10["N10: dwbn_user_m2m_field_updated()"]
        N11["N11: dwbn_user_m2m_field_updated_task()"]
        N12["N12: Country Admin added AND zero admin countries?"]
        N15["N15: admin_changes()"]
        N16["N16: For each Country Admin: home center country missing?"]
        N20["N20: Get home center's country"]
        N21["N21: admin_organisation_countries.add()"]
        N22["N22: update_last_modified()"]

        N6 --> N10
        N10 --> N11
        N11 --> N12
        N7 --> N15
        N15 --> N16
        N12 -->|yes| N20
        N16 -->|yes| N20
        N20 --> N21
        N21 --> N22
    end

    subgraph dwconnect["PLACE: DWConnect — Center Page"]
        N30["N30: findCenterAdmins()"]
        U20["U20: 'Country admins' section"]

        N30 --> U20
    end

    subgraph api["TRIGGER: External API Request"]
        N31["N31: get_object_data()"]
    end

    U21["U21: System email 'From' field"]

    N4 --> S2
    N5 --> S1
    N21 --> S2
    S1 -.-> N15
    S3 -.-> N16
    S3 -.-> N20
    S2 -.-> U5
    S2 -.-> N30
    S2 -.-> N31
    S2 -.-> U21

    classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
    classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
    classDef store fill:#e6e6fa,stroke:#9370db,color:#000
    classDef condition fill:#fffacd,stroke:#daa520,color:#000
    classDef trigger fill:#98fb98,stroke:#228b22,color:#000

    class U1,U2,U3,U4,U5,U6,U7,U20,U21 ui
    class N1,N2,N3,N4,N5,N6,N10,N11,N15,N20,N21,N22,N30,N31 nonui
    class N12,N16 condition
    class N7 trigger
    class S1,S2,S3 store

Example B: Designing from Shaped Parts


Part 1: Shaping Context (Input to Breadboarding)

This section shows what comes FROM shaping — the requirements, existing patterns identified, and sketched parts. This is the INPUT that breadboarding receives.

The R (Requirements)

IDRequirement
R0Make content searchable from the index page
R2Navigate back to pagination state when returning from detail
R3Navigate back to search state when returning from detail
R4Search/pagination state survives page refresh
R5Browser back button restores previous search/pagination state
R9Search should debounce input (not fire on every keystroke)
R10Search should require minimum 3 characters
R11Loading and empty states should provide user feedback

Existing System with Reusable Patterns (S-CUR)

The app already has a global search page that implements most of these Rs. During shaping, it was documented at the parts/mechanism level:

PartMechanism
S-CUR1URL state & initialization
S-CUR1.1Router queryParams observable provides {q, category}
S-CUR1.2initializeState(params) sets query and category from URL
S-CUR1.3On page load, triggers initial search from URL state
S-CUR2Search input
S-CUR2.1Search input binds to activeQuery BehaviorSubject
S-CUR2.2activeQuery subscription with 90ms debounce
S-CUR2.3Min 3 chars triggers performNewSearch()
S-CUR3Data fetching
S-CUR3.1performNewSearch() sets loading state, calls search service
S-CUR3.2Search service builds Typesense filter, calls rawSearch()
S-CUR3.3rawSearch() queries Typesense, returns {found, hits}
S-CUR3.4Results written to detailResult data store
S-CUR4Pagination
S-CUR4.1Scroll-to-bottom triggers appendNextPage() via intercomService
S-CUR4.2appendNextPage() increments page, calls search
S-CUR4.3New hits concatenated to existing hits
S-CUR4.4sendMessage() re-arms scroll detection
S-CUR5Rendering
S-CUR5.1cdr.detectChanges() triggers template re-evaluation
S-CUR5.2Loading spinner, "no results", result count based on store
S-CUR5.3*ngFor renders tiles for each hit
S-CUR5.4Tile click navigates to detail page

Sketched Solution: Parts that Adapt S-CUR

The new solution's parts explicitly reference which S-CUR patterns they adapt:

PartMechanismAdapts
F1Create widget (component, def, register)
F2URL state & initialization (read ?q=, restore on load)S-CUR1
F3Search input (debounce, min 3 chars, triggers search)S-CUR2
F4Data fetching (rawSearch() with filter)S-CUR3
F5Pagination (scroll-to-bottom, append pages, re-arm)S-CUR4
F6Rendering (loading, empty, results list, rows)S-CUR5

Part 2: Breadboarding (Transform Parts into Affordances)

This is where breadboarding happens. The shaped parts become concrete affordances with explicit wiring. The output is the affordance tables and diagram.

UI Affordances

#ComponentAffordanceControlWires OutReturns To
U1letter-browsersearch inputtype-> N1
U2letter-browserloading spinnerrender
U3letter-browserno results msgrender
U4letter-browserresult countrender
U5letter-browserresults listrender-> U6, U7, U8, U9
U6letter-rowrow clickclick-> LD
U7letter-rowdaterender
U8letter-rowsubjectrender
U9letter-rowteaserrender
U10letter-browserscrollscroll-> N11
U11browserback buttonclick-> N9
U12letter-browser"See all X results"click-> LP
LDLetter Detailplace
LPFull Pageplace

Code Affordances

#ComponentAffordanceControlWires OutReturns To
N1letter-browseractiveQuery.next()call-> N2-> U12
N2letter-browseractiveQuery subscriptionobserve-> N3
N3letter-browserperformSearch()call-> N4, -> N6, -> N7, -> N8
N4typesense.servicerawSearch()call-> N3, -> N12
N5letter-browserparentId (config)config-> N4
N6letter-browserloading storewrite-> N8
N7letter-browserdetailResult storewrite-> N8, -> N16
N8letter-browserdetectChanges()call-> U2, -> U3, -> U4, -> U5
N9browserURL ?q=read-> N10
N10letter-browserinitializeState()call-> N1, -> N3
N11intercom.servicescroll subjectobserve-> N12
N12letter-browserappendNextPage()call-> N4, -> N7, -> N8, -> N13, -> N14
N13intercom.servicesendMessage()call-> N11
N14routernavigate()call-> N9
N15letter-browserif !compact subscribeconditional-> N11
N16letter-browserif truncated show linkconditional-> U12
N17letter-browsercompact (config)config-> N4, -> N15, -> N16
N18letter-browserfullPageRoute (config)config-> U12

Mermaid Diagram

mermaid
flowchart TB
    subgraph lettersIndex["PLACE: Letters Index Page"]
        subgraph letterBrowser["COMPONENT: letter-browser"]
            U1["U1: search input"]
            U2["U2: loading spinner"]
            U3["U3: no results msg"]
            U4["U4: result count"]
            U5["U5: results list"]
            U12["U12: See all X results"]

            N1["N1: activeQuery.next"]
            N2["N2: activeQuery sub"]
            N3["N3: performSearch"]
            N6["N6: loading store"]
            N7["N7: detailResult store"]
            N8["N8: detectChanges"]
            N10["N10: initializeState"]
            N16["N16: if truncated show link"]
            N5["N5: parentId (config)"]
            N17["N17: compact (config)"]
            N18["N18: fullPageRoute (config)"]

            subgraph pagination["PAGINATION"]
                U10["U10: scroll"]
                N15["N15: if !compact subscribe"]
                N12["N12: appendNextPage"]
            end
        end
    end

    subgraph letterRow["COMPONENT: letter-row"]
        U6["U6: row click"]
        U7["U7: date"]
        U8["U8: subject"]
        U9["U9: teaser"]
    end

    subgraph browser["BROWSER"]
        U11["U11: back button"]
        N9["N9: URL ?q="]
        N14["N14: Router.navigate"]
    end

    subgraph services["SERVICES"]
        N4["N4: rawSearch"]
        N11["N11: intercom subject"]
        N13["N13: sendMessage"]
    end

    subgraph letterDetail["PLACE: Letter Detail Page"]
        LD["Letter Detail"]
    end

    U1 -->|type| N1
    N1 --> N2
    N2 -->|debounce 90ms, min 3| N3

    N3 --> N4
    N3 --> N6
    N3 --> N7
    N3 --> N8

    N4 -.-> N3
    N4 -.-> N12
    N6 -.-> N8
    N7 -.-> N8

    N8 --> U2
    N8 --> U3
    N8 --> U4
    N8 --> U5

    U5 --> U6
    U5 --> U7
    U5 --> U8
    U5 --> U9

    U6 -->|navigate| LD
    U11 -->|restore| N9
    N9 --> N10
    N10 --> N1
    N10 --> N3

    U10 --> N11
    N15 -->|if !compact| N11
    N11 --> N12
    N12 --> N4
    N12 --> N7
    N12 --> N8
    N12 --> N13
    N12 --> N14
    N13 -->|re-arm| N11

    N5 -.->|filter| N4
    N17 -.-> N4
    N17 -.-> N15
    N17 -.-> N16
    N18 -.-> U12
    N14 -.->|URL| N9

    N1 -.-> U12
    N7 -.-> N16
    N16 -->|if truncated| U12
    U12 -->|navigate with ?q| LP["Full Page"]

    classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
    classDef nonui fill:#d3d3d3,stroke:#808080,color:#000

    class U1,U2,U3,U4,U5,U6,U7,U8,U9,U10,U11,U12,LD,LP ui
    class N1,N2,N3,N4,N5,N6,N7,N8,N9,N10,N11,N12,N13,N14,N15,N16,N17,N18 nonui

Slicing the Breadboard

With the full breadboard complete, slice it into vertical increments. Each slice demonstrates a mechanism working:

Slice Summary

#SliceMechanismAffordancesDemo
V1Widget with real dataF1, F4, F6U2-U9, N3-N8, LD"Widget shows real data"
V2Search worksF3U1, N1, N2"Type 'dharma', results filter"
V3Infinite scrollF5U10, N11-N13"Scroll down, more load"
V4URL stateF2U11, N9, N10, N14"Refresh preserves search"
V5Compact modeU12, N15-N18, LP"Shows 'See all' link"

Slice Diagram

mermaid
flowchart TB
    subgraph slice1["V1: WIDGET WITH REAL DATA"]
        U2["U2: loading spinner"]
        U3["U3: no results msg"]
        U4["U4: result count"]
        U5["U5: results list"]
        U6["U6: row click"]
        U7["U7: date"]
        U8["U8: subject"]
        U9["U9: teaser"]

        N3["N3: performSearch"]
        N4["N4: rawSearch"]
        N5["N5: parentId (config)"]
        N6["N6: loading store"]
        N7["N7: detailResult store"]
        N8["N8: detectChanges"]
        LD["Letter Detail"]
    end

    subgraph slice2["V2: SEARCH WORKS"]
        U1["U1: search input"]
        N1["N1: activeQuery.next"]
        N2["N2: activeQuery sub"]
    end

    subgraph slice3["V3: INFINITE SCROLL"]
        U10["U10: scroll"]
        N11["N11: intercom subject"]
        N12["N12: appendNextPage"]
        N13["N13: sendMessage"]
    end

    subgraph slice4["V4: URL STATE"]
        U11["U11: back button"]
        N9["N9: URL ?q="]
        N10["N10: initializeState"]
        N14["N14: Router.navigate"]
    end

    subgraph slice5["V5: COMPACT MODE"]
        U12["U12: See all X results"]
        N15["N15: if !compact subscribe"]
        N16["N16: if truncated show link"]
        N17["N17: compact (config)"]
        N18["N18: fullPageRoute (config)"]
        LP["Full Page"]
    end

    U1 -->|type| N1
    N1 --> N2
    N2 -->|debounce| N3

    N3 --> N4
    N3 --> N6
    N3 --> N7
    N3 --> N8
    N4 -.-> N3
    N5 -.->|filter| N4

    N6 -.-> N8
    N7 -.-> N8
    N8 --> U2
    N8 --> U3
    N8 --> U4
    N8 --> U5
    U5 --> U6
    U5 --> U7
    U5 --> U8
    U5 --> U9
    U6 -->|navigate| LD

    U11 -->|restore| N9
    N9 --> N10
    N10 --> N1
    N10 --> N3

    U10 --> N11
    N11 --> N12
    N12 --> N4
    N12 --> N7
    N12 --> N8
    N12 --> N13
    N12 --> N14
    N13 -->|re-arm| N11
    N4 -.-> N12
    N14 -.->|URL| N9

    N15 -->|if !compact| N11
    N17 -.-> N4
    N17 -.-> N15
    N17 -.-> N16
    N18 -.-> U12
    N1 -.-> U12
    N7 -.-> N16
    N16 -->|if truncated| U12
    U12 -->|navigate with ?q| LP

    style slice1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style slice2 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
    style slice3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style slice4 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
    style slice5 fill:#fff8e1,stroke:#ffc107,stroke-width:2px

    classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
    classDef nonui fill:#d3d3d3,stroke:#808080,color:#000

    class U1,U2,U3,U4,U5,U6,U7,U8,U9,U10,U11,U12,LD,LP ui
    class N1,N2,N3,N4,N5,N6,N7,N8,N9,N10,N11,N12,N13,N14,N15,N16,N17,N18 nonui