Gists

A synced, readable copy of my gists from gist.github.com/bl9. Each page is the gist content pasted verbatim; the table links to the local page and to the original gist.

14 gists, newest first. Last synced 2026-07-25.

UpdatedGistFile(s)Size
2026-07-25bmad-rcon-howto 🔒bmad-rcon-howto.md42 KB
2026-07-25llm 🔒llm.md70 KB
2026-07-25BMAD Recon 🔒bmad-recon.md99 KB
2026-07-25bmad-rcon-foundation 🔒bmad-rcon-foundation.md43 KB
2026-07-23bmad_Research 🔒bmad_research.md65 KB
2026-07-23Similarity-Calculations 🔒Similarity-Calculations.md33 KB
2026-07-10vector_plot 🔒vector_plot.md4 KB
2026-06-22ios_sensors 🔒ios_sensors.md86 KB
2026-06-11autocmds 🔒autocmds.lua4 KB
2026-06-03skull 🔒skull.md9 KB
2026-06-03tez 🔒prompt.txt, tez.txt73 KB
2026-05-29apache_tez 🔒apache_tez.md36 KB
2026-05-19AI Tools 🔒ai_tools.md8 KB
2024-08-19c_linked_listc_linked_list.c872 B

🔒 = secret gist (unlisted on GitHub).

Two additional gists (codes, gistfile1.txt) hold private recovery phrases and are intentionally kept out of this published section.

Source: bmad-rcon-howto · bmad-rcon-howto.md · updated 2026-07-25 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

Part 1.5 — Concept-by-concept how-to

Companion to bmad-deep-recon-foundations.md (what the terms mean) and bmad-deep-recon-guide.md (task-driven walkthroughs).

What this document is for. The foundations doc explains what a dimension, a topology, or a mode is. This one shows, for each concept, exactly what you type, exactly what comes back, and exactly what you do next. One concept at a time, in the order you'll meet them.

Section 20 does the same thing without BMAD at all — the same techniques applied in a plain Claude conversation, since dimensions, plan gates, source discipline and staleness are portable ideas, not BMAD features.

Fidelity note. Session excerpts are illustrative reconstructions. The interaction shape — what you're asked, what you can say back, what lands on disk — reflects the documented design. Exact wording lives in the skill's reference files and will differ. TOML key paths should be confirmed against your installed customize.toml.


Contents


0. Setup

npx bmad-method install     # pick your IDE; deep-recon ships in core

Verify and look around:

/bmad-customize bmad-deep-recon

Read the options; change nothing yet. You're building a map of what's adjustable.


1. Mode: Draft

What Draft does: composes a research prompt carrying the pack's craft — pruned dimensions, freshness bars, source policy, a hard citation demand — tuned to the external tool you name. You paste it into that tool. No searching happens in your session.

Use it when: you already pay for ChatGPT/Gemini/Perplexity deep research, and the question is a broad public sweep.

What you type

> /bmad-deep-recon

draft a deep research prompt for Gemini: choosing a machine translation
vendor for EN↔FR, EN↔ES, EN↔JA, at roughly 40M characters/month.
Hard requirements: EU and Canada data residency, glossary and
do-not-translate support, and a DPA we can actually sign.
Type: competitive, select shape.

What comes back (shape)

DRAFTED PROMPT — for Gemini Deep Research
─────────────────────────────────────────
Research the commercial machine-translation vendor landscape for a buyer
with the following profile: [your volume, pairs, residency needs].

Cover these dimensions, in priority order:
  1. Pricing structure at 40M chars/month — not list price, the actual
     shape (per-character, committed-use tiers, minimums)
  2. Per-pair quality evidence for EN↔FR, EN↔ES, EN↔JA specifically;
     distinguish vendor-reported from independent
  3. Data residency options for EU and Canada; processing locations
  4. Glossary / do-not-translate / custom terminology feature parity
  5. Contractual posture: DPA availability, sub-processors, training-on-
     customer-data defaults, exit and portability terms

Source requirements:
  - Every claim must carry publisher, publication date, access date
  - Pricing and feature claims older than 6 months: flag as possibly stale
  - Mark vendor self-reported quality claims explicitly as self-reported
  - Prefer primary sources (vendor docs, pricing pages, trust centres)
    over review-aggregator sites

Output: a comparison with an explicit "could not determine" section.
─────────────────────────────────────────
Paste this into Gemini. When you have the report back, run:
   process <path to the report>

What you do

Check three things before pasting. This is the whole skill of Draft mode:

CheckWhyFix if missing
Are your numbers in it?Volume and pairs change the ranking completely. A generic prompt gets a generic rankingAdd them by hand
Is the citation demand hard?A soft demand produces a soft reportStrengthen it: "every claim, no exceptions"
Is the freshness bar stated?Pricing from 2024 is fictionAdd "flag anything older than N months"

Edit the prompt freely. It's a starting point, not an output.

Why it works this way

The value isn't that Deep Recon can write a prompt — you could. It's that the pack's craft gets embedded automatically: the dimension priorities, the source policy, the self-reported-claim flagging, the "could not determine" demand. Those are the things people forget, and they're the difference between a usable report and a marketing summary.


2. Mode: Process

What Process does: takes a finished report from anywhere, files the original untouched, extracts claims into digests behind the firewall, gap-checks against the pack, and distills the standard cited research.md.

Use it when: you have a document — from Draft mode, from an analyst, from a colleague.

What you type

> there's a research report at ~/Downloads/mt-vendors.pdf, process it

What comes back

PROCESSING — competitive pack

  imports/mt-vendors.pdf              preserved, unmodified
  digests/d01-pricing-structures.md   14 claims extracted
  digests/d02-quality-evidence.md      9 claims  (6 flagged self-reported)
  digests/d03-residency.md             7 claims
  digests/d04-terminology-features.md  5 claims

GAP CHECK against the competitive pack
──────────────────────────────────────
COVERED
  pricing, language coverage, published quality claims, API surface

GAPS — not covered by the imported material
  ✗ sub-processor lists and processing-location commitments
  ✗ training-on-customer-data defaults and opt-out terms
  ✗ exit terms and data portability
  ✗ per-pair quality for EN↔JA specifically (only aggregate given)

research.md written — 35 claims, 22 sources, 4 gaps flagged

What you do

Read the gap list, not the report. That's where the value is.

Notice the pattern in that example: every gap is either contractual or specific to you. Hosted research tools crawl broad public commercial surfaces well and are systematically poor at narrow contractual questions and at your particular parameters. That's the division of labour to internalize — and it tells you what to do next:

> run a focused pass on the gaps only — sub-processors, training-on-data
  terms, exit/portability, and EN↔JA quality specifically.
  Effort standard, validation high.

This lands in the same run folder, because the slug is deterministic across the draft → process → refresh lifecycle. One decision, one artifact.

Why it works this way

Preserving the original untouched is provenance: you can always go back to what the tool actually said, rather than trusting the extraction. Extracting behind the firewall means your project context can't shape which claims get pulled out.


3. Mode: Run

What Run does: native research in your session — plan gate, parallel fan-out, verification at landing, cited synthesis.

Use it when: you want it now, or you need tools only your session has (internal MCP sources, your own files, an authenticated API).

What you type

> /bmad-deep-recon

I need to choose a reranking approach for our recommendation retrieval
stage. Decision horizon ~12 months. Candidates to include but not to
confirm: cross-encoder rerank, learned sparse rerank, MMR-only diversity,
no rerank at all.

Constraints: 200ms p99 for the whole retrieval stage, of which rerank can
have at most 40ms. Multilingual content. We self-host; no hosted APIs.

Type: technical, select shape. Effort: standard. Validation: high.

What comes back

The plan gate — covered in detail in §8. You approve or amend, and then:

ROUND 1 — breadth-first, 3 assistants
  [dim 1] latency/throughput ................ 8 sources → digest landed
  [dim 2] quality delta evidence ............ 8 sources → digest landed
  [dim 3] multilingual behaviour ............ 8 sources → digest landed

  verifying load-bearing claims as they land...
  § "Latency and throughput" written

LEADS FROM ROUND 1
  - Two sources disagree on cross-encoder latency by ~3x; the difference
    appears to be batch size and hardware, not the model
  - Learned-sparse rerank quality claims are almost entirely from one
    research group

ROUND 2 — depth on the two leads
  ...

What you do

Watch for two things and speak up:

  • Aggregator pileup. If several digests trace to the same summary site, that's one publisher wearing several hats: "dimension 2 is leaning on aggregators — chase the primary papers."
  • Leads not being chased. Disagreement is the most valuable signal in the run. If round 2 isn't going after it, say so.

You can interject at any point; you don't have to wait for a checkpoint.


4. Type and pack

How to set it. Three ways, in increasing explicitness:

> research the MT vendor landscape                    # type inferred
> competitive research on the MT vendor landscape     # named in prose
> Type: competitive.                                  # stated as a parameter

How to see what a pack changes

Ask before approving:

> what does the competitive pack prioritise, and what are its freshness
  windows?

You'll get the card: dimension priorities, source craft, freshness per claim class. Reading this once for each type you use is 5 minutes well spent — it's how you learn what to add at the plan gate, because you'll see what the pack considers out of scope.

Choosing when it's ambiguous

Your questionTypeWhy not the other one
"What do DeepL and Google Translate actually offer?"competitiveNamed rivals, teardown shape
"How does the localization industry handle post-editing?"domainLearning a field's structure and practice, not comparing vendors
"Which embedding model should we use?"technicalEvaluating a technology area for implementation
"Does EMA-teacher SSL beat contrastive for retrieval?"academic-litThe evidence base is papers
"What do users hate about recommendation feeds?"user-voiceLived experience, reviews, communities
"How big is the legal-tech translation market?"marketSizing an opportunity

When two fit, run two. A vendor decision often needs competitive (what they offer) and domain (what the regulations require of anyone offering it). Merging them gives you a report that's shallow on both, because they want different sources.


5. Decision shape: explore vs select

Two independent axes: type = the subject matter, shape = the form the conclusion takes.

explore — you don't yet know the option space

> technical research: how are teams handling multilingual embedding
  drift as models get re-versioned? Explore shape.

Output: a structured understanding. Approaches, trade-offs, who does what, where the disagreements are. No recommendation, because you haven't defined what you're choosing between.

select — you're choosing, and you know the candidates

> technical research, select shape: choose between cross-encoder rerank,
  learned sparse rerank, and MMR-only, under a 40ms budget.

Output: everything explore gives you, plus a weighted matrix where the weights are your stated constraints.

The mistake, and how to avoid it

Running select too early narrows prematurely. If you name three candidates before you know whether there's a fourth category you've never heard of, the matrix will confidently rank three options out of a space you haven't mapped.

The sequence when you're genuinely unsure:

1. > ... explore shape          # map the space, cheap preset
2. read the report; now you know the real option space
3. > ... select shape, candidates: [what you learned]

Two cheap runs beat one expensive run that answered the wrong question.


6. Dimension

This is the concept with the highest leverage, so here it is as a full interaction.

Seeing them

Dimensions appear at the plan gate. You don't create them from nothing — the pack proposes, you edit.

DIMENSIONS (5)
  1. Latency and throughput under our budget
  2. Quality delta versus no-rerank baseline
  3. Multilingual behaviour and per-language variance
  4. Serving cost and hardware requirements
  5. Implementation and operational complexity

The four operations

DELETE — the one you'll use most.

> Drop dimension 5. We have the eng capacity; complexity isn't a
  differentiator for this decision.

Why: each dimension costs a full fan-out. At deep, cutting one saves roughly 15% of the run. Dimensions that don't bear on the decision aren't neutral — they dilute.

ADD — the one that determines whether the report is useful.

> Add a dimension: behaviour under filtered ANN search. Every benchmark
  I've seen measures unfiltered top-k, but our production path always
  applies filters, and I suspect that's where the approaches diverge.

Why: the pack is generic by design. Only you know your constraints. In that example the addition is the difference between a report that answers your question and one that answers a similar-looking question.

SPLIT — when one dimension is secretly two.

> Split dimension 2. "Quality delta" for us is two separate questions
  with different evidence bases: published benchmark deltas, and
  reported production A/B results. Keep them separate — the benchmark
  literature and the engineering-blog literature don't overlap.

Why: merged dimensions get one brief, one set of sources, and a shallow answer to both halves.

MERGE — when two would hit identical sources.

> Merge 1 and 4. Latency and hardware cost come from the same
  benchmarks; running them separately duplicates the search.

How to tell whether your dimension set is good

Three questions, in order:

  1. Does it span the decision? If you imagine the report coming back complete, is there any way you'd still be stuck? That's a missing dimension.
  2. Is anything here not load-bearing? If a dimension's answer wouldn't change your choice, delete it.
  3. Would two of these send an assistant to the same sources? Merge them.

The test that catches most errors

For each dimension, finish this sentence:

"If this comes back saying ___, I choose differently."

If you can't complete it, the dimension isn't decision-relevant. Delete it or reframe it.


7. Topology

Topology is proposed for you. You mostly review it — but knowing how to override is what lets you fix a badly-shaped run before it costs you 40 minutes.

Seeing it

TOPOLOGY   breadth-first (dimensions are largely independent)

The three, and what each looks like in practice

Breadth-first — the default, and right most of the time.

ROUND 1 — breadth-first, 3 assistants
  [dim 1] ──→ assistant A ──→ digest
  [dim 2] ──→ assistant B ──→ digest      all in parallel
  [dim 3] ──→ assistant C ──→ digest

Use when the dimensions are genuinely independent. You get even coverage across everything.

Depth-first — one question, multiple angles, iterating.

> Use depth-first. This is really one question — whether the reported
  cross-encoder latency numbers hold on our hardware class — and the
  disagreement between sources IS the problem.

ROUND 1 — depth-first, 3 assistants, one question
  assistant A: vendor and library documentation
  assistant B: independent benchmarks and reproductions
  assistant C: production engineering reports
        ↓
  synthesise, identify what's actually in dispute
        ↓
ROUND 2 — chase the disputed mechanism

Use when you have one contested question and the contradiction matters more than breadth.

Straightforward — one assistant, small budget.

> This is a lookup. Straightforward topology, quick preset.
  Just: what's the current license on [model], and has it changed
  in the last 12 months?

Use for facts. Ten agents on an easy question just burns tokens — this is the most common waste in the whole system.

How to tell the proposal is wrong

SignalWhat it meansWhat to say
Breadth-first proposed, but your dimensions all restate one questionThe decision was framed too narrowly to fan out"This is one question — go depth-first"
Depth-first proposed for six separable dimensionsThe framing collapsed things that should be parallel"These are independent — breadth-first"
Six assistants for something you could GoogleOver-provisioned"Straightforward, quick"
Estimate is 50+ minutes for a decision you'll revisit next sprintEffort/reversibility mismatch"Drop to quick"

Why knowing the algorithms helps

BFS and DFS are graph-traversal algorithms (see the foundations doc, §4.6). Their known weaknesses transfer directly:

  • BFS holds the whole frontier in memory → breadth-first research burns more tokens per round. If you're context-constrained, fewer dimensions beats a lower preset.
  • DFS can go far down a wrong corridor → depth-first research on a question that turns out to be a side issue wastes the entire run. Only go depth-first when you're confident the question is central.

8. The plan gate, line by line

The single hard stop. Here's how to read every line of it.

DECISION                                          ← ① is this actually your decision?
  Select a reranking approach for recommendation
  retrieval; 12-month horizon, 40ms budget.

DIMENSIONS (5)                                    ← ② prune, add, split, merge
  1. Latency and throughput
  2. Quality delta vs. no-rerank baseline
  3. Multilingual behaviour
  4. Serving cost and hardware
  5. Implementation complexity

TOPOLOGY   breadth-first                          ← ③ right shape?
EFFORT     standard — 3 assistants / 8 sources
           per round / 2 rounds                   ← ④ matches reversibility?
VALIDATION high; red_team off                     ← ⑤ do you have a prior?
FRESHNESS  benchmarks 12mo · libraries 6mo        ← ⑥ fast-moving field?
ESTIMATE   ~22 minutes                            ← ⑦ matches your patience?

Approve, or tell me what to change.

The seven checks

#CheckBad answer looks likeWhat to say
Is the restated decision yours?Broader or vaguer than what you meant"The decision is narrower: ..."
Do dimensions span it, with nothing dead?Generic list; nothing specific to youDelete / add / split / merge (§6)
Right topology?Fan-out on a lookup"Straightforward" / "depth-first"
Effort matched to reversal cost?deep on a sprint-reversible choice"Drop to quick"
Do you already believe the answer?red_team off when you walked in convinced"Turn red_team on"
Freshness bar tight enough?24-month window on a fast field"Benchmarks older than 12 months are history"
Will you actually stay?45 min when you have 15"Drop to standard"

A realistic reply

Approve with changes:
- Drop 5; complexity isn't a differentiator here
- Split 2 into published-benchmark evidence vs. production A/B reports
- Add: behaviour under filtered ANN search, not just unfiltered top-k
- Keep breadth-first and standard
- red_team on — I'm going in expecting cross-encoder to win, so argue
  against it

Why you get exactly one gate

Constant confirmation trains you to click through. One gate that genuinely matters gets read. Everything after it is light checkpoints. Your attention here is the single highest-value input you give the system — a well-cited report answering a badly-framed question is the most expensive failure mode available.


9. Effort presets and budgets

Setting it

> ... effort: deep
> ... quick preset, this is just a sanity check

The presets

PresetAssistantsSources/roundRounds
quick251
standard382
deep6123

Precedence — memorize this

Your request beats a pinned setting beats the preset. Whatever you say in the moment wins. So you can pin standard in TOML and still say "deep for this one" without editing config.

Overriding one axis only

You don't have to take a preset whole:

> standard preset, but 3 rounds instead of 2 — I expect the first round
  to surface contradictions and I want them chased properly

Changing effort mid-run

> stop after this round and write up what you have — I have less time
  than I thought

The stop-and-write valve exists precisely for this. Files-first means everything already landed is kept.

How to choose

Scale to reversal cost, not to how interesting the question is:

Reversal costPreset
Undo next sprintquick
Weeks of work to undostandard
Months, or contractual, or regulatorydeep + validation high

10. Rounds and leads

A round is one full cycle of parallel searching, followed by reassessment. Leads are what round 1 produces that shapes round 2: contradictions, unexpected connections, gaps.

Watching leads form

LEADS FROM ROUND 1
  - Two sources disagree on cross-encoder latency by ~3x
  - Learned-sparse quality claims trace to one research group
  - No source addresses filtered-search behaviour at all

Steering round 2 — this is where you add most value mid-run

> For round 2: the 3x latency disagreement is the most important thing
  here. Chase the mechanism — I suspect it's batch size and hardware
  class rather than the model. Deprioritise the sparse-rerank lead;
  single-group evidence is enough for me to discount it.

Contradictions are the highest-value lead type. When two credible sources disagree, one of three things is true, and all three are worth knowing:

  1. The claims are about different conditions (usually — and the conditions are the real finding)
  2. One source is wrong (which tells you about that source)
  3. The field genuinely disagrees (which means your decision carries more risk than the report's tone suggests)

When rounds stop

A dimension stops early when its questions are answered or a full round surfaces nothing new. You can force it:

> dimension 3 is done — stop there and move on

11. The firewall

The rule: your project context may shape what gets asked. It may never shape what counts as evidence. Subagents receive only their brief.

Working with it correctly

✅ Our retrieval stage has a 200ms p99 budget and we self-host.
   Given those constraints, what reranking approaches are viable?

Your context became a constraint on the question. Legitimate — that's the context of discovery.

Breaking it by hand — what not to do

❌ Here's our current architecture doc [paste]. Research whether our
   reranking approach is the right one.

You've supplied the conclusion as context. You'll get a well-cited report explaining that your approach is sound. It will be persuasive and worthless.

The tell: if you're pasting a design in and asking "is this right?", stop. Reverse the order:

✅ 1. What do the constraints and evidence say the right approach is?
   2. [read the report]
   3. Now compare your design yourself.

That ordering is the discipline. It costs you nothing and it's the difference between research and justification.

When you genuinely need internal context

Sometimes the research genuinely requires internal sources — your own metrics, an internal wiki, a private API. The firewall doesn't forbid this; it's about where the context enters:

> Use our internal metrics MCP for dimension 1 only — I need our actual
  p99 distribution, not published benchmarks. Keep dimensions 2-4
  external-only.

Scoping internal sources to a specific dimension keeps the rest of the run clean. What you're avoiding is internal context bleeding into dimensions where it would bias the external evidence gathering.

Checking it held

> did any dimension's findings rest on context I supplied rather than
  retrieved sources?

A clean run answers "no." If the answer is anything else, that's a finding about the report's reliability.


12. Verification and red team

Setting the level

> ... validation: high
LevelWhat it doesUse for
normalSpot-checks the claims the recommendation rests onMost runs
highCross-checks the pack's critical claim classes; red-teams major conclusionsLoad-bearing decisions
maxChecks everything; full-breadth red-teamRegulatory, contractual, security

Turning on red team, and why the phrasing matters

> red_team on. I'm going in believing cross-encoder rerank is the right
  answer — argue the strongest case against it.

Naming your prior explicitly gives the adversarial pass a target. "Red team this" is weaker than "here is what I believe; attack it."

The rule for when: turn it on exactly where you already believe the answer. Nobody walks into a security or privacy review neutral — you walk in believing you're fine. That's the condition it exists for. It's off by default because it costs tokens, not because it's optional for high-stakes work.

Challenging a specific claim mid-run

> claim [7] is doing a lot of work in this recommendation and it's from
  a vendor blog. Verify it independently or downgrade it.

What verification is not

The system checking a claim is the same system that produced it. That's a coherence check, not independent corroboration. Useful, weaker than the word suggests. Treat high validation as "the obvious errors were caught," not "this is confirmed."


13. Claims and statuses

Every claim carries a status: unverifiedverifieddisputedoverturned.

Reading them

[4] Cross-encoder rerank adds 35-60ms at batch size 32 on A10-class
    hardware.  [verified · 2 independent sources · pub 2026-02, 2026-04
    · accessed 2026-07-25]

[7] Learned-sparse rerank matches cross-encoder quality at 40% the
    latency.  [disputed · vendor-reported · single research group ·
    pub 2025-11 · accessed 2026-07-25]

What to do with each status

StatusMeaningYour move
unverifiedFound, not checkedFine for background. Never let it be load-bearing
verifiedSpot-checked against another sourceUsable
disputedCredible sources disagreeThe disagreement is the finding. Understand the conditions before choosing
overturnedSuperseded by newer evidenceKept visible on purpose — check whether it fed a downstream decision

Why overturned claims aren't deleted

Because erasing the old belief destroys the audit trail that makes the new one trustworthy. If you can rewrite history, nobody can tell "this was always true" from "someone changed it." Same reason version control doesn't delete old commits.

Challenging the tally

> how many claims in the recommendation section are still unverified?

That count comes from the deterministic script, not the model's estimate — it's a real count.


14. Briefs, digests, memlog

Inspecting a brief — what one assistant was actually told

cat _bmad/planning/rerank-selection-2026-07/briefs/latency-throughput.md

Why look: if a dimension came back thin, the brief usually shows why — it was too broad, or too narrow, or it didn't mention your hardware class. That diagnoses whether to re-run the dimension or reframe it.

Inspecting a digest — what one assistant found

cat _bmad/planning/rerank-selection-2026-07/digests/d01-latency.md

Why look: trace one claim from research.md back through its digest to its source. Do this once, early. After you've seen how a claim in the report relates to what was actually retrieved, you'll never over-trust a polished report again.

Reading the memlog

tail -40 _bmad/planning/rerank-selection-2026-07/memlog

Append-only, so it shows the sequence — including claims whose status changed. ref= and status= entries are what the deterministic script tallies. Last status wins.

Why files-first exists

Everything lands on disk when it exists, not at the end. Consequences you'll actually feel:

  • A run that dies at minute 30 resumes from disk
  • The report builds in front of you instead of behind a spinner
  • You can inspect intermediate work and intervene

15. Staleness, refresh, deepen

The staleness map

At the bottom of every report:

STALENESS MAP
  Fast-moving (re-check ~3 months)
    [4] latency figures — hardware and runtime releases move these
    [11] library versions and API surface
  Medium (~12 months)
    [7] published quality benchmarks
  Slow (~24 months)
    [2] license terms

Turn this into calendar entries the same day. This is the step everyone skips, and skipping it is what turns a living asset into a snapshot.

Refresh — time has passed

> refresh the rerank research
CONFIRMED (9)
CHANGED (2)
  [4] latency improved ~25% after a runtime release
  [11] library API changed; migration guide published
OVERTURNED (1)
  [7] the quality parity claim was retracted by the authors

⚠  Claim [7] is referenced by:
       docs/decisions/0021-reranking.md

That warning is why the lifecycle exists. What you do: open the ADR, note the change, decide whether it changes anything. Often it doesn't. The value is that the question got asked.

Deepen — a dimension was under-answered

> deepen the filtered-ANN-behaviour dimension

Drills one dimension without re-running the rest. Minutes, not tens of minutes.

Choosing between them

SituationCommand
Months have passedrefresh
A dimension was in scope but thindeepen
A genuinely new questionnew run
The decision itself changednew run — the old one's dimensions were built for a different decision

16. The run folder and slug

Finding your runs

ls _bmad/planning/
# rerank-selection-2026-07/
# mt-vendor-selection-2026-07/
# embedding-model-selection-2026-06/

The slug is deterministic — generated by the script, not the model — so draft, process, and refresh of the same decision all land in the same folder.

Anatomy

rerank-selection-2026-07/
├── imports/     # originals, untouched — provenance
├── digests/     # extracted claims — the working layer
├── briefs/      # what each assistant was told — file-based, never shell
├── memlog       # append-only sequence — the truth
└── research.md  # the canonical cited report — the artifact

Where to put it in your repo

docs/decisions/
├── 0021-reranking.md          # ADR: what we chose, and why
└── 0021-research.md           # the evidence, with provenance

The ADR tells you what in six months. The research file tells you whether it still holds.


17. Headless and scripting

output_format = auto renders HTML for interactive runs and plain markdown for headless or skill-invoked ones — no configuration needed.

A quarterly refresh job

# .github/workflows/research-refresh.yml
on:
  schedule: [{ cron: "0 6 1 */3 *" }]
jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          bmad-deep-recon refresh \
            --headless --output-format md \
            --workspace docs/decisions/research/
      - run: ./scripts/alert-on-overturned.sh

The alerting rule that keeps it alive

Alert on overturned, and on changed where the claim is referenced by a decision record. Ignore confirmed.

Get this wrong and you've built a quarterly notification everyone mutes by the second quarter. Get it right and you have decision-rot detection that costs nothing and requires nobody to remember.


18. Configuration in practice

The three layers

FileScopeWins?
_bmad/custom/bmad-deep-recon.user.tomlPersonal, gitignoredYes
_bmad/custom/bmad-deep-recon.tomlTeam, committedMiddle
The skill's customize.tomlDefaultsLowest

Merge rules: scalars override; tables deep-merge; arrays of tables keyed by code/id replace matches and append new ones; other arrays append. No removal mechanism — you override, you don't delete.

A sensible day-one personal config

# _bmad/custom/bmad-deep-recon.user.toml

[workflow]
output_format = "both"
validation    = "normal"
# red_team stays off globally — turn it on per-run, deliberately

A domain source policy — the highest-value override

[[workflow.source_policies]]
code = "regulatory"
preferred = ["eur-lex.europa.eu", "edpb.europa.eu", "ico.org.uk",
             "priv.gc.ca", "cai.gouv.qc.ca"]
banned_kinds = ["law-firm marketing", "compliance-vendor content marketing"]

[[workflow.source_policies]]
code = "security"
preferred = ["nist.gov", "owasp.org", "attack.mitre.org", "nvd.nist.gov"]
banned_kinds = ["security-vendor content marketing"]

Write a policy for a domain you know well. You can already tell good sources from bad in your own field — that expertise is exactly what a source policy encodes, and it's the single most effective configuration you can make.

Confirm key paths against your installed customize.toml; the shape and intent are what matter here.


19. Recovering when things go wrong

SymptomCauseFix
Report reads like a textbookNo decision stated → nothing to prune againstRestate as a decision with constraints; re-run
Report agrees with everything you believedYou pasted your design inRe-run without it, compare afterward yourself
Matrix weights feel genericConstraints weren't stated as constraintsSay them explicitly at the gate; deepen or re-run
A dimension came back thinBrief too broad or too narrowcat the brief to diagnose, then deepen with a sharper framing
Everything traces to two aggregator sitesSource policy too permissiveBan the kind; re-run the affected dimension
Run died partwayContext or session lossResume — files-first means what landed is kept
Took 3x the estimatePreset too high, or too many dimensionsCut dimensions first, preset second
You disagree with the recommendationEither your weights were wrong, or your prior wasFix the weights and say why in the ADR. Don't re-run with new framing until it agrees — turn red_team on instead

That last row is the important one. Re-framing until you get the answer you wanted is the one failure mode the whole system cannot protect you from, because you're driving.


20. All of this in plain Claude, without BMAD

Most of what makes Deep Recon good is discipline, not tooling. You can run a decent approximation in any Claude conversation. Here's the whole method.

20.1 The five moves

Move 1 — State the decision, not the topic.

I'm choosing between cross-encoder rerank, learned sparse rerank, and
MMR-only for a recommendation retrieval stage. Constraints: 40ms budget
for rerank, multilingual content, self-hosted only, 12-month horizon.

Before you research anything: propose 4-6 independent dimensions of
investigation, and tell me which you'd prioritise. Don't search yet.

That last line manufactures your own plan gate.

Move 2 — Prune the plan before any searching.

Drop the "implementation complexity" dimension — not a differentiator
for us. Add: behaviour under filtered ANN search specifically, since
benchmarks measure unfiltered top-k and our path always filters.
Now go.

Move 3 — Demand the source discipline explicitly.

For every claim: publisher, publication date, and note whether it's
vendor self-reported or independent. Flag anything published more than
12 months ago as potentially stale rather than stating it as current.
If several claims trace back to one original source, say so — I want to
know when three citations are really one.

Move 4 — Ask for the negative space. The single highest-value instruction, and the one people never give:

End with two sections:
- What you could not determine, and why
- Which of these claims will go stale fastest, and roughly when I should
  re-check them

Move 5 — Red-team it in a separate turn.

I'm inclined toward cross-encoder. Make the strongest case that I'm
wrong, using the evidence you gathered.

Separate turn matters — asking for a recommendation and its refutation in one breath produces a hedge instead of an argument.

20.2 Keeping the firewall

The discipline is entirely on you here, because there's no architectural barrier:

❌ Here's our architecture [paste]. Is our approach right?
✅ What do the constraints and evidence say? [read] ... then compare yourself.

If you must supply internal context, scope it and say why:

Use this only as a constraint on the question, not as evidence:
our p99 budget is 200ms and we self-host. Don't let it shape which
sources you weight.

Weaker than a real firewall — it's an instruction, not a wall — but stating it makes you notice when you're about to break it, which is most of the benefit.

20.3 Approximating the artifact

Ask for the output as a file with the metadata that makes it durable:

Write this to a markdown file with frontmatter: date, the decision it
supports, and a source list with publication and access dates.

Then commit it next to the ADR, exactly as you would with BMAD output. The lifecycle is the part you can fully replicate without any tooling — a dated file in version control and a calendar reminder from the staleness section gets you most of the refresh mechanism.

20.4 Approximating refresh

Three months later, open the file:

Here's a research file from three months ago. Re-check only the claims
it flags as fast-moving. Tell me: confirmed, changed, or overturned —
and flag anything overturned that the linked decision depends on.

20.5 What you can't replicate

Being honest about the gap:

FeatureReplicable in plain Claude?
Decision framing and dimension pruning✅ Fully — it's a discipline
Source and freshness discipline✅ Fully — it's an instruction
"Could not determine" and staleness map✅ Fully
Red-team pass✅ Fully — just use a separate turn
Durable dated artifact✅ Fully — write the file, commit it
Refresh with delta⚠️ Manually, and you have to remember
True firewall❌ It's an instruction, not an architectural barrier
Deterministic citation cross-check❌ No script counting your markers
Parallel fan-out with separate contexts❌ One context does everything
Deterministic run identity across sessions❌ You manage filenames yourself

The honest summary: roughly 70% of the value is discipline you can adopt today, in any conversation, with no installation. The tooling buys you enforcement, parallelism, and lifecycle — real things, but they're the multiplier on the discipline rather than a substitute for it.

If you take one thing from this document into tomorrow's work, take Move 1 and Move 4: state the decision before the topic, and always ask what couldn't be determined.


21. One-page cheat card

Framing

I'm choosing between X, Y, Z. Constraints: A, B, C. Horizon: N months.
Include those candidates but don't treat the list as the frame.
Type: <market|domain|technical|competitive|user-voice|academic-lit>
Shape: <explore|select>   Effort: <quick|standard|deep>
Validation: <normal|high|max>   red_team: <on|off>
Freshness: claims older than N months are history, not fact.

At the plan gate

Approve with changes:
- Drop dimension N — doesn't bear on the decision
- Add: <the thing only you know matters>
- Split N into <A> and <B> — different evidence bases
- Topology: <breadth-first|depth-first|straightforward>
- red_team on — I'm going in believing <X>

Mid-run

Dimension N is leaning on aggregators — chase the primaries.
Round 2: prioritise the <contradiction>; deprioritise <weak lead>.
Claim [n] is load-bearing and single-sourced — verify or downgrade.
Stop after this round and write up what you have.

Reading the report — in this order

1. Could not determine       4. Staleness map → calendar
2. Selection matrix weights  5. The recommendation, last
3. Self-reported flags

Lifecycle

refresh the <topic> research      # time passed
deepen the <dimension> dimension  # under-answered
/bmad-deep-recon                  # genuinely new question

The sentence that gates everything

"I am choosing between ___, ___ and ___, under constraints ___, ___ and ___, and I'll live with it for ___ months."

Can't write it? You're not ready to research — you're ready to brainstorm.


Companions: bmad-deep-recon-foundations.md (what the terms mean) · bmad-deep-recon-guide.md (task-driven walkthroughs, applied domains, epistemology)

Source: llm · llm.md · updated 2026-07-25 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

How an LLM Actually Works

A mental model for someone who already knows transformers on paper but can't reconcile "matrix multiplications over vectors" with "it understood what I meant."


Contents

Part I — The object

Part II — Putting it together

Part III — How it got that way

Part IV — Applied

Part V — Reference


0. The one thing to unlearn first

If your intuition for "text → vector" comes from embedding models (sentence-transformers, bi-encoders, HNSW indexes), that intuition is actively blocking you.

Embedding modelTransformer LM
Output per inputOne vector for the whole textOne vector per token position
Vector meaningA point in a static semantic spaceA running computation, mid-flight
How many times writtenOnce~80–120 times (once per layer)
Information flowNone between docsEvery position reads from every earlier position, at every layer
NatureA noun — a stored representationA verb — an unfolding process

An embedding is a photograph. A transformer forward pass is a film — and the frames modify each other.

That is the whole reason one can only do retrieval while the other can write a compiler pass.


1. Concrete anchor: a real model's dimensions

Vague talk about "dimensions" is why this feels like magic. Here are actual numbers, using a Llama-3-70B-class model:

d_model (residual stream width)   8,192
layers                               80
attention heads per layer            64
head dimension                      128
MLP hidden width                 28,672
vocabulary                      128,256
context window                  128,000 tokens
total parameters             70,000,000,000

Derived facts worth internalizing:

  • Attention head computations per token: 80 × 64 = 5,120. Not one similarity lookup — five thousand of them, each with its own learned notion of what to look for and what to bring back.
  • Parameter split: roughly 1/3 attention, 2/3 MLP. Most of the model is memory, not routing.
  • FLOPs per generated token: ~2 × 70B = 140 billion. For a 500-token answer, ~70 trillion floating point operations. It is not a lookup. It is a lot of computation.
  • 8,192 numbers per position. In fp16 that's 16 KB. That 16 KB is the model's entire working belief about that word in that context, at that depth.

2. Tokenization — the layer everyone skips, and shouldn't

Text is split into subword tokens by BPE. Rough rule: 1 token ≈ 3.5–4 characters of English.

"the tests are failing"     → ["the", " tests", " are", " failing"]        4 tokens
"unfortunately"             → ["unfort", "unately"]                        2 tokens
"strawberry"                → ["str", "aw", "berry"]                       3 tokens
"getUserByID"               → ["get", "User", "By", "ID"]                  4 tokens
"128256"                    → ["128", "256"]                               2 tokens
"色即是空"                   → ["色", "即", "是", "空"] or worse            4+ tokens

Two consequences that explain real behavior:

  1. The model cannot see letters. "strawberry" arrives as three opaque chunks. Asking it to count the r's is asking someone to count letters in a word they only ever heard spoken. This single fact explains a whole genre of "how is it so dumb about this" failures.
  2. Non-English and code are tokenized less efficiently. The same sentence in Thai or Tamil may cost 3–5× the tokens of English. Since compute is per-token, this is also a fairness and cost issue in multilingual retrieval systems — the tokenizer is a quiet source of bias before any model math happens.

Each token ID indexes a row of the embedding matrix: 128,256 × 8,192. That row is the starting value of the residual stream at that position. It encodes essentially nothing contextual yet — just "this token, generically."

Position is added separately (modern models use RoPE, which rotates query and key vectors by an angle proportional to position, so attention naturally sees relative distance rather than absolute index).


3. The residual stream — the single most important object

Picture a spreadsheet:

  • Columns = token positions in your prompt
  • Rows = layers, 80 of them, bottom to top
  • Each cell = an 8,192-dimensional vector
                 "the"     "tests"    "are"    "failing"
             ┌──────────┬──────────┬──────────┬──────────┐
Layer 80     │ v80,1    │ v80,2    │ v80,3    │ v80,4    │ ← this one becomes the prediction
             ├──────────┼──────────┼──────────┼──────────┤
    ...      │   ...    │   ...    │   ...    │   ...    │
             ├──────────┼──────────┼──────────┼──────────┤
Layer 2      │ v2,1     │ v2,2     │ v2,3     │ v2,4     │
             ├──────────┼──────────┼──────────┼──────────┤
Layer 1      │ v1,1     │ v1,2     │ v1,3     │ v1,4     │
             ├──────────┼──────────┼──────────┼──────────┤
Embedding    │ e(the)   │ e(tests) │ e(are)   │ e(fail…) │
             └──────────┴──────────┴──────────┴──────────┘

Critically, the update rule is additive:

x ← x + attention_output(x)
x ← x + mlp_output(x)

Not x ← f(x). The stream is a shared workspace that every component reads from and writes into. Nothing is overwritten; contributions accumulate.

This is why the mental model of "a bus" or "a whiteboard" is better than "a pipeline." Layer 3 can write a fact that nothing touches until layer 61 reads it. Components communicate across depth by leaving things in the stream.

Two directions of information flow:

  • Vertical (within a column): MLPs. Enrich this position's representation with knowledge.
  • Horizontal (between columns): Attention. This is the only mechanism by which positions see each other. Remove attention and you have 4 independent MLPs that have never heard of one another.

4. Attention, dissected properly

The textbook formula is softmax(QKᵀ/√d)V. The useful decomposition is different — split it into two independent learned circuits:

The QK circuit — "where do I look?"

Each position produces a query ("what am I looking for right now?") and a key ("what am I, advertised to others?"). Dot product, scaled, softmaxed over all earlier positions → attention weights.

This is exactly the retrieval you know from vector search, with three differences that change everything:

  1. The corpus is your own prompt, not a static index.
  2. The query is recomputed from the current stream state at every layer — so a position's question at layer 60 is informed by what it learned at layers 1–59.
  3. It runs 5,120 times per token.

The OV circuit — "what do I bring back?"

Separately, each position produces a value vector, and the head has an output matrix. What gets copied back into the stream is attention_weights × values × W_O.

The decoupling is the point. What matched and what gets copied are different learned functions. A head can attend to the word "France" but write a country-code direction rather than the France representation. Retrieval systems don't do this; they return the document they matched.

Multi-head = parallel specialists

64 heads per layer, each with a 128-dim subspace. They specialize sharply:

Head typeJobObserved in
Previous-token headAttend to position i−1Every model, layer 1–2
Duplicate-token headFind earlier copies of the current tokenEarly-mid layers
Induction headPattern [A][B] … [A] → predict [B]Layers ~2+
Name-mover headCopy a specific name to the output positionMid-late
Syntax headsSubject→verb, noun→adjective, bracket matchingEarly-mid
Suppression headsActively reduce a candidate's probabilityLate

That last row is worth pausing on. Some heads exist to say no. The network implements something like negation and inhibition using attention.

Causal masking

Position i can only attend to positions ≤ i. This is what makes training efficient — one forward pass over a 4,000-token document yields 4,000 training signals at once, each a legitimate "predict the next token" problem. It's also why the whole thing generates left to right.

KV cache — why the first token is slow and the rest are fast

  • Prefill: your whole prompt is processed in one parallel pass. All keys and values for all positions are computed and cached. This is compute-bound and is the delay before the first token appears.
  • Decode: each new token attends against the cache. Only one new column is computed per step. This is memory-bandwidth-bound.

Cache size scales linearly with context length. At 128K context this is tens of GB. This is the real reason long context is expensive — not the attention math, the memory.


5. The MLP — where knowledge is stored

Two-thirds of the parameters live here, and the structure is a key-value memory (Geva et al., 2021):

h = GELU(W_in · x)        # 8,192 → 28,672 : 28,672 pattern detectors
out = W_out · h           # 28,672 → 8,192 : each fires, writes its associated vector back
  • Each of the 28,672 rows of W_in is a key: a direction in stream-space it detects.
  • Each corresponding column of W_out is a value: what to add to the stream when that key fires.

So: "if the stream currently contains [Paris-ness] + [capital-of-relation-active], add [France-direction]."

Multiply by 80 layers: 2.3 million key-value slots, chained so later ones read the results of earlier ones. That's the fact store — and because it's composed across depth, it does multi-hop lookups, not just single retrievals.


6. Superposition — the answer to "how can dimensions do this"

The intuitive objection: 8,192 dimensions can't hold millions of concepts.

The intuition is wrong, and here's the geometry.

In 8,192 dimensions you can only fit 8,192 perfectly orthogonal vectors. But if you relax to "almost orthogonal" — say, any pair within 85–95° — you can fit an exponential number. This is the Johnson–Lindenstrauss lemma. Concretely, in d dimensions you can pack roughly exp(εd) near-orthogonal directions.

At d = 8,192 that number is astronomically larger than the millions of concepts a model needs.

The tradeoff: features interfere slightly. Every feature bleeds a little noise into every other. This is tolerable if only a handful of features are active at any moment — which is exactly the case, since any given token in any given context is only a few things at once. This is sparse coding, and the network learns it because it's the optimal use of limited width.

This has been verified empirically, not just theorized. Anthropic's sparse autoencoder work on Claude 3 Sonnet extracted tens of millions of interpretable directions from the residual stream: a Golden Gate Bridge feature, a "code with a security vulnerability" feature, a "sycophantic praise" feature, an "inner conflict" feature. Amplify one and the model's behavior changes accordingly and predictably.

So the correct reformulation:

A residual stream vector is not a point in an 8,192-concept space. It is a sparse sum of a few dozen active features drawn from a dictionary of ~10⁷ learned directions.

That reframing is what makes "8,192 numbers" stop feeling too small.


7. Circuits — worked examples of the machinery

These aren't hypotheses. They've been reverse-engineered and causally verified by ablation.

7a. Induction heads — the origin of in-context learning

Setup. A two-head circuit spanning two layers:

  1. A previous-token head in layer L: at each position, copies the identity of the token before it into the stream. So position i now carries "I am preceded by X."
  2. An induction head in layer L+1: its query is "find positions preceded by the token I currently am." It matches, then copies that position's own token to the output.

Net effect: given [A][B] … [A], predict [B].

Worked example — why the model can track your made-up variable names:

const zqBuffer = new ArrayBuffer(1024);
...200 lines...
const view = new Uint8Array(zq▮

zqBuffer appears nowhere in training data. There is no stored fact. Yet the completion is Buffer, near-certainly.

Trace:

  • Position of the final zq emits a query: "where else have I seen zq?"
  • Duplicate-token / previous-token machinery has tagged the earlier occurrence.
  • The induction head attends to the token after the earlier zq — which is Buffer.
  • OV circuit copies Buffer into the output stream.
  • Unembedding turns it into a very high logit.

Why this matters far beyond variable names: induction heads generalize from exact-match copying to fuzzy, abstract pattern completion. The same circuit family is what lets few-shot prompting work. When you write:

sea otter -> loutre de mer
peppermint -> menthe poivrée
plush girafe ->

induction-style heads recognize the X -> Y structure, identify the transformation as translation, and apply it. No weights changed. The "learning" happened entirely as data movement inside one forward pass.

There is a striking empirical detail: during pretraining, induction heads form abruptly at a specific point, and the loss curve visibly bends at exactly that moment. In-context learning is not a smooth emergent haze — you can watch the mechanism switch on.

7b. The IOI circuit — indirect object identification

Prompt: "When Mary and John went to the store, John gave a drink to ___"

Correct answer: Mary. Both names appear; the model must pick the one that isn't the repeated subject. The full circuit was reverse-engineered in GPT-2 small (Wang et al., 2022) — 26 heads in three groups:

GroupFunction
Duplicate-token headsDetect that John appeared twice
S-inhibition headsWrite a signal that suppresses attention to John
Name-mover headsAttend to the remaining name and copy it to the output

So the algorithm is literally: find all names → detect which one is duplicated → inhibit it → copy the survivor.

This is a genuine algorithm, discovered by gradient descent, implemented in attention patterns, verified by ablating individual heads and watching the answer flip to John. Nobody designed it. Nobody wrote "if duplicate then suppress." It was the lowest-loss way to model English discourse.

When you ask "how does it know what I meant" — this is the shape of the answer. Thousands of circuits like this, composed.


8. Depth = composition, and where intent physically lives

A rough map of what happens as you climb the 80 layers:

LayersWhat forms
0–5Detokenization: reassembling word pieces, resolving word sense, basic syntax
5–25Phrases, entities, grammatical relations, factual lookups
25–55Abstract task representation, relationships between entities, register and intent, plan formation
55–75Concretizing the plan into specific content
75–80Converting to a distribution over the 128K vocabulary

The middle band is the interesting one, and there's direct evidence for it.

Task vectors / function vectors

Take a few-shot prompt for some task (say, "translate English to French"). Read the residual stream at a mid layer at the final position. Average across many examples. You get a single vector.

Now take a completely unrelated prompt, with no examples and no instructions, and add that vector into the stream at the same layer.

The model starts translating to French.

Let that land. There is a specific vector, in a specific place, that is the concept "the task right now is: translate to French," represented independently of any wording that expressed it. You can extract it, store it, transplant it.

This is the mechanistic answer to "how does it know what I meant." Your intent stops being words and becomes a vector somewhere in the middle of the network. Different phrasings of the same request converge to nearby vectors — which is exactly why "make this faster," "this is too slow," and "optimize perf here" all get you the same kind of help.

Logit lens — reading the model's mind mid-computation

You can apply the final unembedding matrix to intermediate layers to see what the model would predict if it stopped there. Typical trajectory for a factual question:

Layer 10 : generic function words ("the", "a", "of")
Layer 30 : right category, wrong specifics ("city", "place", "region")
Layer 55 : right answer starting to appear, low confidence
Layer 70 : right answer dominant
Layer 80 : sharpened, high confidence

The prediction is built up, progressively refined. It is not looked up.


9. From vector to token: the output side

At the last position, after layer 80:

  1. Unembed: logits = W_U · x → 128,256 raw scores.
  2. Softmax with temperature: p = softmax(logits / T). T=0 → always the argmax (deterministic). T=1 → the raw learned distribution. T>1 → flatter, more random.
  3. Truncate: top-k (keep the k best) or top-p / nucleus (keep the smallest set summing to p, typically 0.9–0.95). This cuts the long tail of garbage tokens.
  4. Sample one token.
  5. Append it to the input and run the entire thing again.

That last step is the loop, and it's why the model can't un-say something. Each token is committed and becomes input. There's no backtracking, no edit buffer.

Dissecting an actual distribution. For the prompt "The tests are failing after I added the cache":

token          logit    prob
" layer"        18.2    0.41
" ."            17.6    0.23
" to"           16.9    0.11
" middleware"   16.1    0.05
" and"          15.8    0.04
" invalidation" 15.4    0.03
...128,250 more tokens, together ~0.13

Two things to notice:

  • The model is never certain. Even "obvious" continuations carry substantial mass elsewhere. Confidence is a distribution, not a boolean.
  • The whole answer is present in embryo. The distribution already encodes "this is a technical debugging context, in English, mid-sentence, about caching." Every subsequent token narrows further.

10. Full worked trace

Prompt: "the tests are failing after I added the cache layer"

Note what is not in that sentence: no question mark, no request, no "please help," no specification of what output you want. Yet you'd get back a ranked list of likely causes with debugging steps. Let's trace how.

Prefill — layers 1 through 80, at the final position

DepthWhat accumulates in the stream
L1–4Token pieces reassembled. " cache" is disambiguated — CPU cache? cache-money? Attention to " tests", " layer" resolves it toward software caching.
L5–12Syntax: tests is the subject, failing the predicate, after establishes temporal-causal ordering. added the cache layer binds as a completed action by the speaker.
L12–25Domain features light up: software-engineering, test-suite, caching, regression. Related knowledge is pulled in by MLPs: cache invalidation, TTL, memoization, test isolation, singletons, mocking.
L25–40Discourse-intent features. This is the crucial band. The stream acquires: speaker-is-blocked, implicit-request-for-diagnosis, causal-hypothesis-expected, speaker-is-a-developer, register-is-technical-peer, not-a-beginner (they said "cache layer," an architectural term).
L40–60Response plan. Features for enumerate-probable-causes, ordered-by-likelihood, include-diagnostic-steps, format-as-list, assume-familiarity-with-testing-tools. Simultaneously, specific hypotheses get ranked: shared cache state leaking across tests, cache not cleared between runs, stale reads, serialization of cached objects, timing/TTL flakiness, a singleton surviving teardown.
L60–75Concretization. Abstract "shared state leaks across tests" becomes lexical material — words like isolation, teardown, fixture, singleton gain probability mass.
L75–80Projection to vocabulary.

Where did the "request" come from?

Nowhere in the text. It was inferred, and here's the mechanism, not the hand-wave:

During pretraining, the model saw millions of instances of this exact discourse pattern — a Stack Overflow post, a Slack thread, a GitHub issue, an IRC log — where someone states a symptom and the very next thing in the document is a diagnostic response. To minimize prediction loss on those documents, the model had to learn: "symptom stated by developer" → "diagnosis follows."

Post-training then sharpened which of the many possible continuations it produces (a helpful diagnosis rather than, say, a snarky reply or another user's unrelated question), but the underlying inference was already there in the base model.

Your intent was reconstructed from statistical regularities in how humans structure conversations. Not from a rule. Not from an intent classifier. From compression.

Decode — dissecting the actual answer

Suppose the response begins:

"A few likely culprits, roughly in order of how often they bite:

  1. Cache state leaking between tests — the cache isn't cleared in teardown, so test B sees what test A wrote."

Dissect where each part came from:

FragmentOrigin
"A few likely culprits"The enumerate-causes plan feature from L40–60, plus a register feature choosing casual-technical over formal
"roughly in order of"An epistemic-hedging feature — the model represents its own uncertainty and, post-RLHF, expresses it
"1."Format feature; list structure was decided at L~50, before any token was emitted
"Cache state leaking between tests"Highest-ranked hypothesis. This ranking came from frequency in training data — this is genuinely the most common cause, and the model absorbed that base rate
"isn't cleared in teardown"MLP knowledge: cache + test + state-leakteardown is the associated concept
"test B sees what test A wrote"Concretization — the abstract feature rendered as a minimal example, a style pattern learned from good technical writing

Nothing here was retrieved. There is no stored answer to "tests failing after cache layer." Every token was computed. The reason it's right is that the machinery for reasoning about cache-and-test interactions is genuinely encoded in the weights, in the form of features and circuits that generalize.


11. Why next-token prediction produces intent modeling

This is the philosophical crux, and it's actually rigorous.

The setup. Minimize −log P(next token | all previous tokens) over ~15 trillion tokens of human-written text.

The critical observation: that text was not generated randomly. It was generated by humans with goals, knowledge, moods, expertise levels, and plans. The text is a downstream shadow of those hidden variables.

So to predict it well, you must model the hidden variables. There is no shortcut:

  • To predict the next line of a Stack Overflow answer, you must model what the asker needs.
  • To predict the next token of a legal brief, you must model the argument being constructed.
  • To predict the closing brace of a C function, you must model the scope structure.
  • To predict the punchline, you must model what the audience finds surprising.
  • To predict the next move in a chess transcript, you must model the position.

And memorization is unavailable. 70B parameters versus 15T training tokens is roughly 200 tokens compressed per parameter. A lookup table is not on the table. The only representation that fits is general machinery.

This gives the chain:

accurate prediction → requires compression → requires generalization →
requires modeling the generating process → and the generating process is
a human with an intent

Intent modeling is not a feature that was added. It is the lowest-loss solution to the objective, and gradient descent found it because it was cheapest.

The same argument explains capabilities that look unrelated to text: arithmetic, code execution traces, spatial reasoning, theory of mind. Each is a compression win on some slice of the corpus.


12. In-context learning — learning without learning

Weights are frozen at inference. Nothing is stored. Yet:

Prompt:
  gxlfj -> 5
  hqm   -> 3
  aabbccdd -> 8
  zzz ->

Answer: 3. The task ("count characters") was never named, never trained on with these strings, and no gradient was computed.

What actually happens: induction-style heads recognize the input -> output structure, mid-layer circuits form a task representation from the demonstrations, and later layers apply it to the new input. The forward pass implements a learning algorithm.

There's evidence that in some settings this is literally gradient descent implemented inside the attention layers — a linear-regression task in-context produces internal updates that approximate one or more gradient steps on that task. The transformer learned to run an optimizer as a subroutine, because doing so lowers prediction loss on documents containing patterns.

Practical corollary: your prompt isn't a query. It's a program that reconfigures the model's effective computation. That's why prompt structure matters so much — you're not phrasing a search, you're building a temporary specialist.


13. Chain of thought = renting more serial compute

Fixed architecture means fixed serial depth: 80 layers. Some problems require more sequential steps than that, no matter how wide the model is.

The escape hatch is the output itself. Every emitted token is fed back as input, so:

serial computation available = 80 layers × number of tokens generated

Writing intermediate steps turns the context window into external working memory and buys unbounded serial depth.

Direct:  "What is 47 × 83?"  →  "3,901"        ← one 80-layer pass. Often wrong.

CoT:     "47 × 83
          = 47 × 80 + 47 × 3
          = 3,760 + 141
          = 3,901"                              ← ~40 tokens = ~3,200 layers of serial work

Each line is computed, then read back as ground truth for the next line. This is why "think step by step" works, and why reasoning-tuned models generate long internal traces. They're not being verbose — they're allocating compute.

Important caveat: the written trace is not guaranteed to be the actual causal path. Models can reach an answer internally and then generate a plausible-looking justification. Chain of thought is a compute mechanism and a post-hoc narrative, and telling those apart is an open research problem.


14. Training: SGD as a compiler

14.1 The reframe: the training loop is the compiler

If you know compilers, you already have the right structure in your head — it just sits one level up from where you're looking.

gcc pipeline
  C source  ──►  gcc  ──────────►  binary  ──►  CPU
  human-written  passes by hand     ~5 MB       can reject

LLM pipeline
  15T tokens ──►  SGD  ──────────►  weights ──►  forward pass
  human-written  passes by search   ~140 GB     never rejects

Same shape. Two differences carry all the weight:

  1. Nobody wrote the passes. In gcc, a human wrote constant folding, a human wrote loop unrolling, and each is provably correct. In an LLM, a stochastic search over a 70-billion-dimensional program space found the passes. The 80 layers genuinely contain bracket matching, scope tracking, type propagation, dead-branch suppression — but they were discovered, not authored, and nobody has fully read them back.
  2. The runtime has no reject state. More on this in §14.9; it's the single most clarifying fact about why the thing "accepts everything."

The closest thing in your world is superoptimization — searching the space of instruction sequences for one better than a human would write. SGD is superoptimization with a different objective, a vastly larger search space, and no proof of anything.

14.2 What one SGD step actually does

Forget "training" as a vague process. A single step is small and entirely mechanical. Say the batch contains this snippet scraped from GitHub:

def process(items):
    result = []
    for item in items:
        result.append(transform(item))
    return ▮

Step 1 — forward pass. The model produces a distribution over all 128,256 tokens at that position. Suppose it assigns p(" result") = 0.31.

Step 2 — loss. Cross-entropy is just the negative log of the probability assigned to the actual next token:

loss = −ln(0.31) = 1.17 nats

If it had been confident:  −ln(0.95) = 0.05
If it had been clueless:   −ln(1/128256) = 11.76

The loss is literally "how surprised were you." Nothing more.

Step 3 — backward pass. Backpropagation computes ∂loss/∂w for every one of the 70 billion weights — one number per weight saying "nudge me this direction to have been less surprised." This is the chain rule applied to a computation graph, executed in reverse.

Step 4 — update.

w ← w − learning_rate × ĝ

With a learning rate around 1e-4 (decaying on a cosine schedule), each weight moves by roughly one part in ten thousand. In practice the optimizer is AdamW, which keeps running averages of the gradient and its variance so each weight gets its own effective step size.

That's the entire step. In isolation it is nearly worthless — the model became microscopically less surprised by one snippet. Everything comes from doing this ~10⁶ times over ~10¹³ tokens.

14.3 Why one update improves a million unrelated cases

This is the crux, and it has no compiler analogue.

To predict result correctly, the network had to run a real inference chain:

  • result was bound at the top of this scope
  • it's an accumulator — mutated in a loop, never otherwise read
  • Python functions conventionally return their accumulator
  • we are at return, in this function's scope, not the enclosing one

The gradient does not reward "the string result." It rewards whichever circuits performed that inference: the variable-binding tracker, the scope-boundary detector, the accumulator-pattern recognizer.

And those circuits are shared. The identical variable-binding machinery fires for:

  • Rust let bindings
  • SQL aliases in a CTE
  • pronoun resolution in English prose
  • "the aforementioned party" in a contract
  • a defined term in a mathematical proof

So one Python snippet measurably improves the model's handling of legal documents. Not by analogy — literally, the same weights, updated once, better everywhere.

This is the generalization mechanism, and it explains the otherwise baffling fact that 15 trillion tokens of miscellaneous internet text produces something that can write a compiler pass. There's a well-documented empirical consequence: adding code to the training mixture improves reasoning on non-code benchmarks, because code is unusually dense in explicit logical structure and, crucially, was checked by a compiler before being committed.

14.4 Why it cannot memorize, even if it wanted to

15,000,000,000,000  training tokens
    70,000,000,000  parameters
──────────────────────────────────
              ~200  tokens compressed per parameter

At 2 bytes per parameter in fp16, that's roughly 0.01 bytes of storage per training token. Lossless memorization is off the table by three orders of magnitude.

SGD is therefore under brutal compression pressure, and the resolution is the one you'd reach yourself if you were hand-writing a codec: store the rules, not the instances.

  • A generative rule for "how Python scoping works" costs a few thousand parameters and covers billions of tokens.
  • Memorizing those same tokens costs billions of parameters and generalizes to nothing.

Gradient descent finds the first solution because the second doesn't fit. Generalization is not an achievement the model unlocks. It is the only thing that fits in the space available.

This is also the cleanest answer to "how does it know almost everything." It doesn't store everything. It stores the generative structure of everything, and re-derives specifics at inference time — which is exactly why it's fluent on common things and confidently wrong on rare ones.

14.5 The numbers, for calibration

Tokens per batch~4 million (thousands of sequences in parallel)
Optimizer steps~10⁵–10⁶
Sequence length during training4K–32K, extended later
Total FLOPs~6 × params × tokens ≈ 6×10²⁴
Hardware10⁴–10⁵ accelerators, weeks to months
Costtens to hundreds of millions of dollars
OptimizerAdamW — needs ~2 extra states per weight, so ~3× the model's memory just to train
Passes over the data~1. It sees most tokens exactly once.

That last row deserves a pause. There is no drilling, no repetition, no curriculum of the same material. The model learns Python scoping from seeing millions of different correct examples one time each — which is precisely the setup that forces it to learn the rule instead of the instances. Repetition would encourage memorization; the single pass forbids it.

The 6 × params × tokens figure decomposes as 2 for the forward pass and 4 for the backward pass (backward costs roughly twice forward). Handy for estimating any training run.

14.6 The loss curve has structure

Training loss is not a smooth glide. Capabilities arrive as phase transitions:

loss
 │╲
 │ ╲___                    learning token frequencies, basic syntax
 │     ╲
 │      ╲__               ← visible bend: induction heads form
 │         ╲___             in-context learning switches ON here
 │             ╲__        ← arithmetic circuits consolidate
 │                ╲____
 └──────────────────────► tokens seen

The induction-head bend is the famous one. Before it, few-shot prompting essentially doesn't work. After it, it does. You can watch a mechanism switch on by looking at a loss curve — the capability is not a smooth emergent haze, it's a circuit forming.

This is the same shape as grokking: a network can sit at memorization-level performance for a long time and then abruptly generalize, when the general circuit finally beats the memorized lookup on the loss. You are watching a search find a better program and swap it in.

14.7 Where the modern gains actually come from

Architecture is roughly frozen. A 2019 transformer with modern normalization and RoPE is close to a 2026 one. Two things moved instead:

1. Data curation — the real secret sauce, and the least published part.

  • Aggressive deduplication (near-duplicate web pages are actively harmful — they induce memorization)
  • Quality classifiers trained to recognize "textbook-like" writing
  • Mixture ratios: how much code vs. web vs. books vs. math vs. multilingual, tuned empirically
  • Annealing: saving the highest-quality data for the last few percent of training, where the learning rate is tiny and the effect is disproportionate. The model's final impressions are its most durable ones.
  • Synthetic data: using a strong model to generate clean training material for the next one

2. RLVR — reinforcement learning from verifiable rewards. This is the stage that made code good, and it's the one a compiler person will find most familiar:

sample N candidate solutions
  ↓
run the test suite / check the proof / execute the program
  ↓
reward the trajectories that passed
  ↓
gradient-ascend toward those

Unlike RLHF ("did a human prefer this?"), the reward here is ground truth. You can optimize against it hard without immediately Goodharting, because a test suite has no taste to exploit. This is why RLVR adds genuine capability while RLHF mostly adds manners.

It also explains the capability profile you observe in practice: models are disproportionately strong in domains with cheap automated verification (code, math, formal logic) and weaker where correctness is a matter of judgment.

14.8 The post-training stages in detail

StageDataWhat it addsWhat it doesn't
Pretraining~15T tokens of web, books, codeEssentially all knowledge, reasoning ability, world model, intent modelingAny notion of being an assistant. A base model given a question is as likely to write more questions.
SFT~10⁴–10⁶ curated dialoguesThe assistant format; the mapping "user turn → helpful response"New knowledge, in any meaningful quantity
Reward modelingHuman preference comparisons (A vs. B)A learned scorer that approximates human judgmentAnything, directly — it's scaffolding for the next stage
RLHF / RLAIFThe reward model's scoresTone, helpfulness, honesty, refusal behavior, calibrated hedgingNew capabilities
RLVRMath/code with automatic checkingGenuine reasoning improvementAnything where correctness isn't checkable

The mental model for the middle three: the base model can simulate an enormous range of authors. Post-training doesn't teach it anything new; it selects which author it becomes when addressed as an assistant, and makes that selection reliable.

The cost asymmetry is stark — pretraining is 98%+ of the compute, post-training is a rounding error. Yet post-training is what makes the artifact usable. It is a thin, cheap layer of steering over an enormously expensive engine.

The known downside: RLHF optimizes a proxy for human approval, and approval is not truth. This is the mechanistic origin of sycophancy — reversing a correct position under pushback is what the reward signal rewarded.

14.9 The disanalogy that resolves "it accepts everything"

You're conflating two properties. Separate them and most of the mystery dissolves.

Property 1: "It accepts anything." This is architectural, and not impressive.

Look at where a compiler can fail versus where a transformer can:

Compiler stageCan fail?Transformer equivalentCan fail?
Lexeryes — invalid characterBPE tokenizerno — total function, always emits tokens
Parseryes — syntax error(none exists)
Type checkeryes — type error(none exists)
Linkeryes — undefined symbol(none exists)
Codegenyessoftmaxno — always a valid distribution

There is no code path in a transformer for "I don't understand this input." No grammar, no parser, no error state, no partial-failure mode. Every stage is a total function over its input domain. Feed it line noise and it will confidently predict the next token of line noise.

That's not intelligence — it's the absence of a rejection mechanism. And it's the direct architectural cause of hallucination: a system that cannot fail to parse also cannot fail to answer. When the model doesn't know something, there is no representational slot for "unknown" and no branch to take. The output head produces a peaked distribution regardless, because that's the only thing it can do.

Everything that looks like the model saying "I'm not sure" is learned behavior from post-training, generated as ordinary tokens — not a structural check. It's a string, not a status code.

Property 2: "It knows almost everything." This is capacity plus compression — and it's less true than it feels.

It knows the high-frequency structure of nearly everything, and degrades smoothly into the tail:

frequency in training data
       high ──────────────────────────────► low
       │                                     │
   reliable                            plausible
   and correct                         and wrong
       │                                     │
  "how does a         "LLVM             "the signature of
   dominator           dominator          getAnalysisUsage
   tree work"          tree API"          in LLVM 17.0.3"

And the confidence is flat across that entire axis, because nothing in the architecture measures how well-supported a fact was. The useful heuristic that falls out: trust its structure, verify its specifics. Algorithms and architecture generalize from circuits. API signatures, version numbers, and citations are memorized facts, and memorized facts decay with training frequency and go stale with time.


15. Worked scenario: "build me a landing page for a dog-walking service"

Why does this work, when the exact page was never in training data?

The abstractions that exist in the weights:

  • landing-page schema: hero, value proposition, social proof, pricing, CTA, footer
  • local-service-business schema: service area, trust signals, booking mechanism
  • HTML/CSS syntax circuits: matched tags, valid property names, correct nesting
  • Design conventions: contrast, hierarchy, whitespace, mobile-first

What the forward pass does:

  1. L25–45: composes landing-pagelocal-servicepet-industry into a joint plan. Pet industry contributes warm colors, photography-forward, informal-but-trustworthy tone.
  2. L45–65: instantiates concrete sections in order.
  3. Decode: emits HTML. Here the syntactic circuits do heavy lifting — an open <div> creates a strong attention signal that must eventually be discharged by </div>. This is bracket-matching implemented as attention, and it's why generated code is structurally valid even when semantically wrong.
  4. Each token conditions the rest. Having written a hero section with a specific headline, the CTA later in the page will echo its phrasing — because that's what coherent documents do, and coherence is what was optimized.

Where it breaks and why: it will confidently produce a plausible-looking page that may use a CSS property that doesn't exist, or a framework API from a version that changed. It's generating from a distribution over plausible code, and plausible ≠ correct. It has no compiler.


16. Worked scenario: a compiler pass

Prompt: "Write an LLVM pass that eliminates redundant loads."

Knowledge assembled from the MLPs: SSA form, dominance relations, alias analysis, LoadInst and StoreInst APIs, the runOnFunction signature, memory dependence.

Reasoning assembled from circuits: the actual algorithm — walk the dominator tree, track available loads, kill on any aliasing store, replace dominated redundant loads with the earlier value.

Why it can do this: the LLVM codebase, its documentation, dozens of textbooks, and thousands of blog posts are in the training data. But more importantly, the abstractions generalize: "dominance," "available expressions," "kill sets" are structures the model represents, not strings it memorized. Ask for the same pass targeting a different IR and it adapts, because the structure transfers.

Where it breaks: it will get the algorithm right and the API version wrong. Reasoning circuits generalize; specific API signatures are memorized facts, and memorized facts decay with training-data frequency and go stale with time. The intuition to carry: trust its structure, verify its specifics.


17. Failure modes, and what each one reveals about the architecture

Failures are the best evidence for the mechanism, because they map onto architectural constraints exactly.

FailureRoot cause
Can't count letters in "strawberry"Tokenization — it never sees characters
Arithmetic on large numbersFixed serial depth; digits are badly tokenized. CoT partially fixes it
Confident wrong facts (hallucination)Sparse-in-training-data facts have weak feature representations, but the output head still produces a peaked distribution. Fluency and truth are separate axes
Loses the thread in very long contextsAttention mass is finite and spread over more positions; "lost in the middle" is real
Repetition loopsLow-temperature sampling entering a self-reinforcing attractor
Fails at genuinely novel deep reasoningNo search, no backtracking. Layer-by-layer refinement isn't tree search
Reverses its position under pushbackPost-training partially selected for agreeableness — Goodharting on human preference
Cites a real-looking but nonexistent paperAuthor, venue, year, and title-style are each high-probability independently; the joint doesn't exist

Notice that none of these are random. Every one falls out of a structural property. That's a good sign your mental model is correct — a correct model predicts the failures.


18. What is honestly still unknown

Intellectual honesty matters here, because a lot of writing on this topic overclaims.

Established: the architecture; superposition; specific circuits (induction, IOI, and dozens more); feature extraction via sparse autoencoders; task vectors; scaling laws.

Not established:

  • No end-to-end account of any complex behavior. Nobody can trace why a specific 500-token compiler answer came out correct. We have footholds, not a map.
  • Why in-context learning is as good as it is. Mechanisms are known in pieces; the full story isn't.
  • Whether chain of thought is faithful to the actual computation.
  • What the mid-layer "planning" representations really are — evidence exists that models plan several tokens ahead (e.g. picking a rhyme word before writing the line that leads to it), but the general structure is unclear.
  • Whether anything like understanding is present in a sense beyond functional. This is partly empirical and partly a question about what the word means, and pretending it's settled in either direction is unjustified.

19. Every concept, defined and modeled

Each entry: what it literally is, then the mental model that makes it make sense. Where a compiler analogy helps, it's there.

19.1 The substrate: numbers and shapes

TermWhat it literally isMental model
ParameterOne number, learned during training. A 70B model has 70 billion.A single knob. Meaningless alone — like asking what byte 4,182,993 of a compiled binary "means." Meaning lives in groups.
WeightA parameter used as a multiplier on an input. The overwhelming majority of parameters.How strongly one thing influences another. Learned "conductance" in a circuit.
BiasA parameter added, not multiplied. A constant offset.A default lean. "Assume this is slightly true unless told otherwise." Many modern models drop them — barely matters.
Weight matrixA 2D grid of weights, e.g. W_in at 8,192 × 28,672.A learned transformation from one space to another. Each row is a pattern being detected; each column is a pattern being written.
ActivationA number computed during a forward pass. Not learned; depends on your input.The runtime values. Weights are the program, activations are the stack and registers.
NeuronOne dimension of an MLP's hidden layer. 28,672 per layer.A single pattern detector with a learned trigger and a learned response. Individually usually polysemantic — it fires for several unrelated things (see superposition).
TensorAn n-dimensional array. Scalars, vectors, matrices generalized.Just the shape of the data. [batch, sequence, d_model] is the shape you'll see everywhere.
Matrix multiplicationThe core operation. Rows dotted with columns.Asking "how much does this vector resemble each of these learned patterns?" — thousands of dot products at once. GPUs exist for this.
d_modelThe width of the residual stream. 8,192 in our reference model.The bandwidth of the shared workspace. How much information one position can carry at once.
PrecisionBits per parameter: fp32, bf16, fp8, int4.How finely each knob is calibrated. bf16 is the training default. Inference often drops to int8/int4 — cheaper, slightly lossier.

19.2 Architecture

TermWhat it literally isMental model
TokenA subword unit, ~3.5–4 chars. The model's atomic symbol.The model's alphabet. It cannot see below this level — the reason letter-counting fails.
VocabularyThe full token set, ~128K entries.The model's fixed symbol table, built once by BPE before training.
BPEByte-pair encoding: iteratively merge the most frequent adjacent pair.A learned compression scheme for text. It is a total function — unlike a lexer, it can never reject input.
Embedding matrixvocab × d_model. One learned vector per token.The lookup that converts a symbol into a starting vector. Context-free — pure "this token, generically."
Positional encoding / RoPERotates Q and K vectors by an angle proportional to position.How the model knows word order. RoPE makes attention naturally see relative distance, which is why it extrapolates to longer contexts.
Layer / blockOne attention sublayer + one MLP sublayer. 80 of them stacked.One optimization pass — except nobody wrote it. Each reads the workspace and writes back.
Residual streamThe running vector at each position that every sublayer adds into.The shared whiteboard. The single most important object. Not overwritten, only accumulated.
Skip / residual connectionx ← x + f(x) instead of x ← f(x).What makes the whiteboard a whiteboard. Also what lets gradients reach layer 1 from layer 80 without vanishing — the reason deep networks train at all.
Attentionsoftmax(QKᵀ/√d)VLearned retrieval, recomputed at every layer. The only mechanism moving information between positions.
Query"What am I looking for right now?"Your search query — but recomputed at every layer from the current state.
Key"What am I, advertised to searchers?"The index entry.
Value"What gets copied if you match me."The payload — deliberately decoupled from the key. You can match on "France" and return a country-code vector.
Attention headOne independent Q/K/V/O set, 128 dims. 64 per layer.One specialist doing one job: previous-token, duplicate-detection, name-moving, bracket-matching. 5,120 of them per token.
Attention patternThe softmaxed weight matrix — who attended to whom, how much.The wiring diagram for this specific input. Fixed weights, variable data flow. This is the programmability.
Causal maskPosition i can't attend to j > i.Enforces left-to-right. Also why one document yields thousands of training signals in one pass.
GQA / MQAMultiple query heads sharing fewer key/value heads.A KV-cache size optimization. Pure engineering; almost no capability cost.
MLP / FFNW_out · GELU(W_in · x). ~2/3 of all parameters.A key-value memory. ~28,672 slots per layer: "if you see pattern P, add fact F." Where knowledge lives.
Activation functionGELU, SwiGLU — a nonlinearity.The thing that makes 80 layers more expressive than one. Without it, stacked linear maps collapse into a single linear map.
LayerNorm / RMSNormRescale a vector to a standard magnitude.Automatic gain control. Keeps values in a workable range so training doesn't explode. Boring but load-bearing.
Unembedding / LM headd_model × vocab matrix at the very end.Projects the final vector onto every possible token. Often tied to the embedding matrix.
LogitsRaw pre-softmax scores, one per vocab entry.Unnormalized evidence for each token. Differences matter; absolute values don't.
Context windowMax tokens processable at once. 128K–2M.Working memory. Hard-walled. Nothing outside it exists — no gradual forgetting, just a cliff.
MoE (mixture of experts)Many MLP "experts"; a router activates only a few per token.Decouples stored knowledge from compute per token. A 400B MoE may activate only 30B per token — big brain, cheap thinking.

19.3 Training

TermWhat it literally isMental model
Loss−ln P(actual next token)."How surprised were you?" Zero means certain and right. That's the entire objective.
Cross-entropyThe formal name for that loss.Measures the gap between the model's distribution and reality.
Perplexityexp(loss)."Effectively how many tokens was it choosing between?" Perplexity 10 = as confused as a fair 10-way guess. More intuitive than nats.
Gradient∂loss/∂w — one number per parameter.A per-knob arrow: "turn me this way to be less wrong." 70 billion arrows per step.
BackpropagationThe chain rule applied backward through the computation graph.Blame assignment. Distributes responsibility for the error across every weight that contributed. Costs ~2× the forward pass.
SGDTake a batch, compute gradients, step downhill. Repeat.Blind hill-descent in 70-billion-dimensional space, one tiny step at a time. Stupid per step; extraordinary in aggregate.
Adam / AdamWSGD plus per-parameter running averages of gradient and variance.Adaptive step sizes — cautious on noisy knobs, bold on consistent ones. Costs 2 extra states per weight (~3× memory to train).
Learning rateStep size multiplier, ~1e-4, decaying.How far to move per step. Too high: divergence. Too low: never arrives. The most important hyperparameter.
Warmup + decayRamp the LR up over early steps, then cosine-decay to near zero.Ease in while the model is fragile; fine-tune gently at the end. The final low-LR phase is when high-quality data matters most (see annealing).
BatchThe set of sequences processed before one update. ~4M tokens.Averaging over many examples so the gradient points at a real trend, not one document's noise.
Gradient accumulationSum gradients over several forward passes before stepping.Simulating a huge batch when it won't fit in memory.
EpochOne full pass over the dataset.Mostly irrelevant here — LLM pretraining does ~1 epoch. Each token is seen once. That's what forces rules over memorization.
CheckpointA saved snapshot of all weights.A build artifact. What actually gets shipped.
OverfittingMemorizing training data instead of learning the pattern.Barely a concern at LLM scale — there's no capacity to memorize 15T tokens. The compression ratio is the regularizer.
Scaling lawsLoss falls as a predictable power law in params, data, and compute.Loss is forecastable before you spend the money. Chinchilla's finding: optimal is ~20 tokens per parameter — most early models were badly undertrained.
EmergenceA capability appearing abruptly at some scale.Usually a circuit forming (see induction heads), sometimes an artifact of a threshold-based metric. Real, but often overstated.
GrokkingSudden jump from memorization to generalization after long training.The general circuit finally out-competing the lookup table on loss. Watching search swap in a better program.
PretrainingNext-token prediction on ~15T tokens.Where all knowledge and reasoning comes from. 98%+ of the compute.
SFTSupervised fine-tuning on curated dialogues.Teaching the format, not the content. Selects "helpful assistant" from the range of authors the base model can simulate.
Reward modelA model trained on human A/B preferences to score outputs.A learned approximation of human taste. Scaffolding for RLHF.
RLHF / RLAIFRL against the reward model's scores.Manners, tone, refusals, calibrated hedging. Adds almost no capability. Optimizing approval, not truth — hence sycophancy.
RLVRRL against automatic verification — tests pass, proof checks.The one post-training stage that adds real capability, because the reward is ground truth and has no taste to exploit.
DistillationTraining a small model on a large model's outputs.Compiling with the big compiler, shipping the small binary. Most small fast models are distilled.
LoRA / PEFTTrain small low-rank adapter matrices; freeze the base.A patch file instead of a rebuild. Cheap, swappable, adds style and domain shape — not new reasoning.
Catastrophic forgettingFine-tuning on narrow data degrades general ability.Overwriting shared circuits. The reason naive fine-tuning often makes a model worse overall.

19.4 Inference

TermWhat it literally isMental model
Forward passInput → 80 layers → logits. No weights change.Running the compiled program. Same weights every time; the data flow is what your prompt reconfigures.
PrefillProcessing your whole prompt in one parallel pass.The compile step before output starts. Compute-bound. This is your time-to-first-token.
DecodeGenerating one token at a time, reusing the cache.The execution loop. Memory-bandwidth-bound, not compute-bound — which is why batching helps throughput so much.
KV cacheStored keys and values for all prior positions.Memoization. Without it you'd recompute the whole prompt per token. Grows linearly with context — tens of GB at 128K. The real cost of long context.
SamplingDrawing a token from the output distribution.The one deliberately nondeterministic step. Why the same prompt gives different answers.
TemperatureDivide logits by T before softmax.A confidence dial. T=0: always argmax, deterministic. T=1: the model's honest distribution. T>1: flattened, more surprising, more wrong.
Top-k / top-pKeep only the k best tokens, or the smallest set summing to probability p.Truncating the garbage tail. Top-p ≈ 0.9–0.95 is the usual default.
Greedy decodingAlways take the highest-probability token.Deterministic, and often worse — it produces flat, repetitive text and can dead-end into loops.
Beam searchTrack multiple candidate continuations.Standard in translation, mostly abandoned for open generation — it collapses into bland high-probability text.
Stop sequenceA string that halts generation.An explicit terminator, since the model has no intrinsic sense of "done" beyond a learned end-of-turn token.
PromptEverything in the context before generation starts.Not a query — a program. It reconfigures which circuits fire. That's why structure matters more than politeness.
System promptInstructions placed at the front of context.Highest-leverage position: everything else attends back to it. Not privileged by the architecture, only by position and training.
Zero-shot / few-shotNo examples / a handful of examples in the prompt.Few-shot works via induction-head machinery — pattern completion, not learning. Weights never move.
In-context learningAcquiring a task from the prompt alone.The forward pass implementing a learning algorithm. Sometimes literally approximating gradient descent inside the attention layers.
Chain of thoughtGenerating intermediate reasoning tokens.Renting serial compute. Fixed 80-layer depth × N tokens generated. The context window becomes external working memory.
Speculative decodingA small model drafts several tokens; the big one verifies in parallel.Branch prediction. Free speedup when the draft is right, no loss when it isn't.
QuantizationDropping to int8/int4 at inference.Shrinking the binary. Big memory and speed wins, small quality cost.

19.5 Interpretability

TermWhat it literally isMental model
FeatureA direction in activation space corresponding to a human-meaningful concept.The real unit of meaning — not the neuron. "Golden Gate Bridge," "code with a security bug," "sycophantic tone."
SuperpositionStoring more features than dimensions using near-orthogonal directions.The answer to "how can 8,192 numbers hold everything." ~10⁷ directions packed into 8,192 dims, only a few dozen active at once.
PolysemanticOne neuron firing for several unrelated things.The symptom of superposition. Why reading individual neurons fails.
MonosemanticOne unit, one clean concept.The goal of feature extraction — recovered by sparse autoencoders, not found in raw neurons.
Sparse autoencoder (SAE)An overcomplete autoencoder trained to reconstruct activations sparsely.The decoder ring. Unpacks superposed activations into millions of individually interpretable features.
CircuitA set of components implementing a specific algorithm.A discovered subroutine. Induction heads, the IOI circuit. Real algorithms nobody wrote.
Induction headA two-layer circuit doing [A][B]…[A] → [B].Why in-context learning works. Also why the model tracks variable names it has never seen.
AblationZeroing a component and measuring the damage.#ifdef 0 on part of the network. How circuit claims get causally verified rather than just observed.
Activation patchingCopying activations from one run into another.A/B testing internals. Isolates which component carries which information.
Logit lensApplying the output head to intermediate layers.Reading the model's mind mid-computation. Shows the answer being built, not looked up.
Task / function vectorA mid-layer vector encoding "the task is X," extractable and transplantable.Your intent, as a physical object. Add it to an unrelated prompt and the model performs the task.

19.6 Practical and operational

TermWhat it literally isMental model
FLOPsFloating-point operations.The currency. Inference ≈ 2 × params per token. Training ≈ 6 × params × tokens. Everything else is bookkeeping.
TTFTTime to first token.Dominated by prefill — i.e. by prompt length. Long prompts cost you latency before a single word appears.
Tokens/secDecode throughput.Bounded by memory bandwidth, not FLOPs. Batching many requests together is nearly free on compute.
Throughput vs latencyTotal tokens across all users vs. speed for one.Directly in tension. Big batches maximize throughput and hurt individual latency.
RAGRetrieve documents, insert into the prompt, generate.Giving the model an open book. Fixes staleness and citation, does nothing for reasoning ability.
Fine-tune vs. prompt vs. RAGThree ways to specialize.Prompt = configuration. RAG = knowledge injection. Fine-tune = behavior and format. Most "we need to fine-tune" problems are prompt or RAG problems.
Context length vs. costCost is superlinear-ish in context.Attention is O(n²) in compute, KV cache is O(n) in memory. The memory usually bites first.
DeterminismSame input, same output?Not by default — sampling is stochastic, and even at T=0, GPU floating-point non-associativity across varying batch sizes can flip a near-tie. Bit-exact reproducibility is genuinely hard.

19.7 Behavior and failure

TermWhat it literally isMental model
HallucinationFluent, confident, false output.Not a bug in the usual sense. There is no reject state. A system that cannot fail to parse cannot fail to answer. Weak features still produce peaked distributions.
SycophancyCaving to pushback, over-agreeing.RLHF optimizing a proxy for approval. Approval and truth are correlated but not identical, and the gap is where this lives.
GoodhartingOptimizing the measure until it stops measuring the thing.Why RLVR (ground-truth reward) is safer to push hard than RLHF (taste-based reward).
Lost in the middleDegraded recall of mid-context information.Attention mass is finite and softmax-normalized. More positions, less each. Beginnings and ends survive best.
Repetition loopThe model cycling the same phrase.Low-temperature sampling entering a self-reinforcing attractor — each repeat raises the probability of the next.
Prompt injectionUntrusted text in context issuing instructions.The architectural consequence of having no privilege separation. Your system prompt and a hostile web page are the same kind of tokens in the same stream. There is no const in the context window.
JailbreakPrompting past post-training guardrails.Post-training is a learned bias over a base model that can simulate anyone. It's a strong prior, not a wall.
Knowledge cutoffThe date training data ends.The build date. Everything after is invisible unless supplied in context.

Appendix A: the compressed mental model

  1. Your text becomes tokens; tokens become vectors.
  2. Those vectors are a shared workspace, one column per position, and every layer adds to rather than replaces them.
  3. Attention moves information sideways — learned retrieval, recomputed 5,120 times per token, where "what to match" and "what to copy" are separate learned functions.
  4. MLPs add knowledge vertically — ~2.3 million learned key-value slots.
  5. Each vector holds a sparse combination of millions of learned features, packed via near-orthogonality. Width was never the constraint.
  6. Depth is composition. By mid-network, your intent exists as an extractable vector, independent of your wording.
  7. The final vector projects to a distribution over 128K tokens; one is sampled; the loop runs again.
  8. It knows what you meant because predicting text written by intentional agents requires modeling intent, and compression made that the cheapest available strategy.
  9. Generating tokens buys serial compute; that's what chain of thought is for.
  10. It fails exactly where the architecture says it should. That's the strongest evidence the model above is right.

Appendix B: the analogy that actually holds

Not a search engine. Not a database. Not a person.

A very large, very fast, learned interpreter — running a program written by gradient descent, on data supplied by your prompt. The weights are the program. Your prompt is the input and a runtime reconfiguration of which parts of the program execute. The output is what that program computes.

The reason it feels like understanding is that the program it learned is, in substantial part, a model of the people who wrote its training data — including their intentions.


Further reading (primary sources)

  • A Mathematical Framework for Transformer Circuits — Elhage et al., 2021
  • In-context Learning and Induction Heads — Olsson et al., 2022
  • Toy Models of Superposition — Elhage et al., 2022
  • Interpretability in the Wild (the IOI circuit) — Wang et al., 2022
  • Transformer Feed-Forward Layers Are Key-Value Memories — Geva et al., 2021
  • Scaling Monosemanticity — Anthropic, 2024
  • Function Vectors in Large Language Models — Todd et al., 2023
  • Training Compute-Optimal Language Models (Chinchilla) — Hoffmann et al., 2022

Source: BMAD Recon · bmad-recon.md · updated 2026-07-25 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

BMAD Deep Recon — Complete Guide

Covers PR #2611 (merged 23 Jul 2026) and docs/explanation/deep-recon.md. Written for someone with zero BMAD background, then applied to two concrete features: user recommendation and multilingual translation.

Reading order. bmad-deep-recon-foundations.md — what every term means and where its name came from. Then bmad-deep-recon-howto.md — concept-by-concept, exactly what you type and what comes back (plus how to apply the same techniques in plain Claude, without BMAD). Then this document — applied domains, epistemology, and task-driven walkthroughs.


Contents


0. The short, honest answer to "how useful is this?"

Deep Recon is decision infrastructure, not feature-building infrastructure. It pays off only when the following are all true:

  • You face a choice with real switching cost (a model, a vendor, an architecture, a metric definition, a locale strategy).
  • The answer is not in your repo — it lives in vendor docs, benchmark papers, pricing pages, forum threads, standards documents.
  • You will otherwise decide from the model's memory, which is stale and uncited.

It is not useful for:

  • Writing the ranking service itself (that's bmad-dev / story workflows).
  • Anything answerable by reading your own codebase (that's bmad-document-project).
  • Facts you already know cold.

Practical read for your two examples:

FeatureDeep Recon valueWhy
User recommendation (in an existing OpenSearch/HNSW stack)High — but not where you'd guessIndex config, feature availability and latency budget are internal. The external decisions that matter are privacy and regulatory (§13), adversarial robustness (§14), offline-metric conventions, and licensing (§17). Embedding-model selection ranks below all of those, because it's the one you can cheaply A/B.
Multilingual translationHighVendor/model landscape moves fast, pricing is public and comparable, language-coverage and quality claims are published and contested, and locale/normalization rules are external standards.

Rule of thumb: if you can write the decision as "X vs Y vs Z, and I'll be stuck with it for 12 months," Deep Recon is worth 20–40 minutes. If you can write it as "how do I implement…", it isn't.


1. Background: BMAD concepts you need first

If you already know BMAD v6/v7, skip to §2.

1.1 What BMAD is

BMAD-METHOD ("Breakthrough Method for Agile AI-Driven Development") is an open-source framework that packages agentic software development into skills — structured markdown instruction sets an AI coding agent (Claude Code, Cursor, Copilot, etc.) loads on demand. It imposes a lifecycle: analysis → planning → architecture → implementation.

1.2 The vocabulary

TermMeaning
SkillA directory containing SKILL.md (the entry point) plus supporting files. The unit of installation and invocation. Invoked as /bmad-<name>.
SKILL.mdThe router/entry file. Modern BMAD keeps it lean and defers detail to references/. Every token in SKILL.md is paid on every invocation.
references/*.mdProcedure files loaded just-in-time, only when that branch is taken. This is the token-optimization pattern.
customize.tomlPer-skill config. Merged through a layered resolver (see §9).
AgentA skill with a persona (e.g. Mary the Analyst). Agents expose a menu of options that route to workflow skills.
WorkflowA multi-step skill. Older ones sharded into step-01…step-NN.md; newer ones use mode-based reference files.
Core skill vs module skillsrc/core-skills/ ships to every install. src/bmm-skills/ ships only with the BMM (software dev) module.
ShimA stub skill that forwards an old ID to a new one so existing habits don't break.
Subagent / assistantA spawned sub-context with its own token budget, given a narrow brief, returning a digest.
Planning artifactsThe output folder where analysis/planning docs land, consumed by downstream skills.
MemlogAn append-only run log on disk that survives context loss — the durable record.

1.3 Why "research" is a skill at all

Two failure modes drove it:

  1. Hallucinated grounding. Ask a model about market size or vendor pricing and it answers from training data — confidently, uncitably, and often 18 months stale.
  2. Uncaptured reasoning. Even when the research is good, it evaporates when the chat ends. Downstream planning has to re-derive it.

Deep Recon addresses both: no conclusion may rest on training data alone, and every engagement writes a durable cited research.md.


2. What PR #2611 actually changed

2.1 Consolidation

Three legacy skills — bmad-market-research, bmad-domain-research, bmad-technical-research — totalling ~5,136 lines of near-duplicate step files, were replaced by one skill, bmad-deep-recon, at roughly 650 lines. The three old names survive as v6 shims that forward with the corresponding type pre-set.

2.2 Relocation to core

The skill was moved from src/bmm-skills/1-analysis/ to src/core-skills/. Rationale: research is not code-project-specific (same precedent as brainstorming), so non-software and core-only installs get it too. On core-only installs, {planning_artifacts} falls back to {output_folder}. It also ships as a standalone marketplace plugin.

2.3 The v2 rework (the important part)

The first version was field-tested and found slow, token-heavy, and locally biased, with end-of-run verification degrading quality and subagent digests stranded in contexts that then died. The rework:

  • Replaced the "acquisition mode" / engine-delegate registry with three modes: Draft, Process, Run.
  • Added the research firewall.
  • Went files-first — digests land on disk on arrival.
  • Moved verification to landing time, not an end pass.
  • Rescaled presets faster (standard is now 3 subagents / 8 sources / depth 2).
  • Cut SKILL.md from ~3,989 to ~2,091 tokens by carving procedures into references/.

2.4 Determinism kit

scripts/recon_kit.py (stdlib-only, PEP 723, unit-tested) owns work the model shouldn't hand-derive:

  • Citation marker ↔ appendix cross-check
  • Memlog claim tally (ref= / status= convention, last status wins)
  • Staleness re-check dates from per-class freshness windows
  • Deterministic run-folder slugs across the draft→process→refresh lifecycle
  • Escaped/validated source-appendix HTML

This is the transferable lesson: anything mechanical and verifiable should be a script, not a prompt. Models are bad at counting and date math and excellent at judgment.

2.5 Security findings addressed in review

Worth knowing because the same mistakes appear in your own agent tooling:

FindingFix
{brief} interpolated inside a double-quoted shell template — $(), backticks, and quote-breaking could reach a command line, amplified by --dangerously-skip-permissionsContract made file-based: briefs are written to {doc_workspace}/briefs/<dimension>.md, templates substitute {brief_file}. Researched/imported text never enters a command line.
HTML briefing rendered source URLs unvalidatedAllowlist to http(s) only; escape source-derived text before insertion
Refresh/Deepen reset verification status for out-of-scope claimsStatuses preserved outside the refresh scope
user-voice pack ranked community posts above surveysTriangulate instead; redact usernames, handles, emails, identifying URLs from verbatim quotes
Parallel dimension writes raced on research.mdExplicit single-writer rule: digests return to the lead, which alone writes in plan order

3. The three modes

ModeWhat happensYou supply
DraftComposes a research prompt carrying the type pack's craft — pruned dimensions, freshness bars, source policy, a hard citation demand — tuned to the tool you nameOne paste into ChatGPT / Gemini / Grok / Perplexity
ProcessFiles a finished report into imports/ untouched, extracts claims into digests behind the firewall, gap-checks against the pack, distills the standard cited summary with metadata frontmatterThe report, from any source
RunNative research in session: plan gate → parallel fan-out → verification at landing → cited synthesisApproval at one plan gate

Choosing

SituationMode
You already pay for a deep-research product and don't mind one round-tripDraft → Process
You already have a report (analyst PDF, colleague's doc, whatever)Process
You want results now, one sitting, no app switchingRun
Research needs internal sources or MCP tools only your session can reachRun
Broad public sweep first, targeted follow-up secondDraft + Process, then a focused Run on the gaps

The trade, stated plainly: hosted deep-research products crawl wider per dollar because you've already paid for the subscription; Run costs your tokens and minutes but stays in context and can use every tool your harness has. On a bare "research X" ask, Deep Recon states this trade once and remembers your preference for the session.


4. Research types (the "packs")

A type selects a pack: a ~25-line card of prioritized dimensions, source craft, and freshness rules. Deep Recon infers the type from your ask, or you name it.

TypeUse when
marketSizing an opportunity, segments, pricing, go-to-market
domainLearning an industry or field: structure, players, rules, vocabulary
technicalEvaluating a technology area, integration approaches, implementation reality
competitiveTearing down named competitors: offers, pricing, trajectory, sentiment
user-voiceWhat users actually experience and want: reviews, communities
academic-litLiterature review, state of the art, grounding an approach in papers

Custom types can be added via override TOML through bmad-customize.

Decision shape — a second, independent axis

  • explore (default) — build understanding
  • select — layers a weighted-matrix method over any type for a structured choose-between

Any type can end in a selection matrix. technical + select is the combination you'll use most for engineering decisions.


5. How a native Run thinks

Plan gate (decision, dimensions, topology, effort, time estimate)
        │ you approve
        ▼
   Topology choice
   ├── breadth-first ....... assistants split independent sub-questions
   ├── depth-first ......... assistants take different angles on one question
   └── straightforward ..... one assistant, small budget
        │
        ▼
Digests written to disk as each assistant returns
        │
        ▼
Verify load-bearing claims as material lands
        │
        ▼
Write the dimension's section (report grows in front of you)
        │
        ├── leads + budget remain? → follow the leads (next round)
        └── coverage or exhaustion → next dimension
        │
        ▼
Synthesis: cross-dimension insights, recommendations, staleness map
        │
        ▼
Mechanical citation check → research.md (+ optional HTML briefing)

5.1 The plan gate

The single hard stop. It shows the decision, the dimensions pruned to it, the topology, the knobs in force, and a realistic time estimate. Approve it and the run proceeds with light checkpoints rather than constant interrogation.

This is where you spend your attention. A bad plan gate produces a beautiful, well-cited, useless report.

5.2 Topology

Fan-out is a deliberate choice, not a default. Independent sub-questions get parallel assistants. One deep question gets several perspectives on the same material. A simple lookup gets one assistant with a handful of calls — ten agents on an easy question just burns tokens.

5.3 Effort presets

PresetAssistantsSources / roundRounds
quick251
standard (default)382
deep6123

Precedence: request > pinned knob > preset. Anything you say in the request overrides.

Rounds follow leads — contradictions and unexpected connections from round one become round two's assignments. A dimension stops early when its questions are answered or a full round surfaces nothing new.

5.4 Other run craft

  • Per-assistant tool-call budgets — prevents one assistant from eating the whole run.
  • Short-query craft with an evaluate-before-next-call loop — read the result before firing the next search.
  • Shared source-quality card — answer engines (Perplexity et al.) count as one publisher; chase their citations to the primaries.
  • Stop-and-write valve — commit what you have rather than spiraling.

6. Why the reports hold up

6.1 Two standing rules, inherited verbatim by every subagent

  1. No conclusions from training data. Model memory proposes questions and search strategy; every claim in the report traces to a source retrieved or imported during this engagement.
  2. The research firewall. Your project files and briefs shape what gets asked, never what gets found. Assistants receive only their assignment. persistent_facts defaults to empty. A run therefore cannot come back quietly confirming whatever your local context already believed.

The firewall is the single most important idea in the whole design. Confirmation bias is the default failure mode of agentic research: give an agent your architecture doc and ask it to research alternatives, and it will find that your architecture is excellent.

6.2 Citation discipline

Every claim carries a publisher, a publication date, and an access date. Inline [n] markers resolve to a source appendix. A mechanical cross-check at finalize confirms markers and appendix agree — script, not vibes.

6.3 Verification levels

LevelBehavior
normal (default)Spot-checks the claims the recommendation actually rests on
highCross-checks the pack's critical claim classes and red-teams major conclusions
maxChecks everything; red-team pass at full breadth

Verification runs as material lands, never as an end-of-run rewrite pass (the v1 lesson — a late pass degraded quality). The red-team pass is the single adversarial mechanism. An outcome adjusts a claim's status; it never licenses rewriting findings.

6.4 Freshness as part of truth

Each pack sets freshness windows per claim class. A market size from three years ago is reported as history, not as fact. This matters enormously in fast-moving areas — an MT quality benchmark from 2023 describes a world that no longer exists.

6.5 Files-first

Digests, extractions, and report sections hit the run folder the moment they exist. The conversation is a control channel, not storage. A run that dies mid-flight resumes from disk with nothing lost, and the report builds in front of you instead of behind a spinner.


7. The run folder, and research as a living asset

Each engagement gets one folder under your planning artifacts:

<planning_artifacts>/<run-slug>/
├── imports/      # originals, untouched
├── digests/      # extracted claims, behind the firewall
├── briefs/       # per-dimension assignments (file-based, never shell-interpolated)
├── memlog        # append-only truth
└── research.md   # the canonical cited report

research.md always exists as the machine-readable report. The HTML briefing is its regenerable face — this is the v7 artifact protocol: memlog = truth, markdown = distillation under contract, HTML = face. output_format = auto | html | md | both; auto renders HTML for interactive runs and markdown only for headless or skill-invoked runs.

The report ends with a staleness map naming which claims age fastest and when to re-check. That map powers the lifecycle:

  • Refresh — re-verifies only stale claims, appends a delta report (confirmed / changed / overturned), and warns you when an overturned claim feeds a downstream artifact.
  • Deepen — drills into one dimension without re-running the rest.

The overturned-claim warning is the sleeper feature. If your PRD's rationale rests on a pricing claim that just changed, you find out.


8. Where it sits in the BMAD lifecycle

bmad-deep-recon  →  research.md (+ metadata frontmatter)
        │
        ├──→ bmad-product-brief   (reads the summary; never reprocesses the original)
        ├──→ bmad-prd
        └──→ architecture / ADRs

Downstream skills consume the summary and metadata directly. That's the payoff of the fixed output shape — a PRD doesn't care whether the research came from Draft+Process or a native Run.

Entry points: /bmad-deep-recon, or through the Analyst agent (Mary), whose menu gained TS (select shape), CR (competitive), and UV (user-voice) alongside the existing research entry.


9. Configuration

9.1 Resolution layers

Per-skill config resolves through a layered merge:

  1. _bmad/custom/{skill-name}.user.toml — personal, gitignored — wins
  2. _bmad/custom/{skill-name}.toml — team/org, committed
  3. The skill's own customize.toml — defaults

Merge rules: scalars override; tables deep-merge; arrays of tables keyed by code or id replace matching entries and append new ones; all other arrays append. There is no removal mechanism — you can't delete a base item, only override it (e.g. by code with a no-op) or fork the skill.

9.2 Deep Recon's knobs

KeyPurpose
validationnormal / high / max
red_teamAdversarial pass; default off
use_workflowsWhether to invoke other workflows
subagent_modelsWhich model each assistant class uses
output_formatauto / html / md / both
external_sourcesMCP research sources — shipped examples: Tavily, Perplexity Sonar, xAI X-Search
source policiesPreferred and banned sources
doc standards, handoffsDownstream contract
custom typesAdd your own packs

Use /bmad-customize bmad-deep-recon rather than hand-authoring TOML.


10. Research checkpoints across a feature's life

Most teams treat research as a single event at the start of a project. It isn't. A feature passes through several gates, each with a different question, a different type pack, and a different cost of getting it wrong.

#GateThe questionType + shapeCost of skipping
0ValidateDo users actually want this? What already exists?user-voice, competitiveYou build something nobody asked for
1Define successHow is this measured, and does the metric predict anything real?academic-litYou optimize a number that doesn't move the business
2Build vs. buyIs there a vendor? What's the exit cost?competitive + selectEighteen months rebuilding a commodity
3Legal to ship?Lawful basis, disclosure, residency, retention, transparency dutiesdomainRedesign after legal review, or a regulator finds it
4Attack surfaceAbuse vectors, adversarial ML, supply chaintechnicalSecurity review blocks launch; or it doesn't and you learn later
5Licensing & procurementCan we actually use this model/library/vendor?domain / competitiveChosen model turns out to be non-commercial; vendor has no DPA
6Component selectionWhich model, index, library, framework?technical + selectReversible, usually — the cheapest gate to get wrong
7Failure modesWhat broke for everyone else who shipped this?technicalYou rediscover known incidents in production
8Cost at scaleWhat does this cost at 100× current volume?technicalUnit economics collapse post-launch
9RefreshDid the ground move under a decision we already made?refreshSilent decision rot

The pattern worth internalizing: almost everyone does gate 6 and skips gates 3, 4, and 5 — then discovers them during security and legal review, after the design is frozen and the sprint is committed. Gates 3–5 are cheap in week one and brutal in week ten. They're also the gates where external evidence matters most and where model memory is least reliable, which is exactly the profile Deep Recon is built for.

Gate 6 — the one everybody does — is the one where research adds the least, because you can usually just try both.


11. The full use-case catalog

The categories below are where Deep Recon earns its cost on feature work. Each maps to a type pack and a signal that tells you it's worth running.

#Use caseType + shapeRun it whenTypical miss if skipped
1Feature validationuser-voiceBefore committing a quarter to a surface nobody complained aboutBuilding a feature that solves a problem users don't have
2Competitive teardowncompetitiveA rival shipped the thing you're about to shipReinventing, or missing table-stakes behavior
3Metric & eval methodologyacademic-litYour metric will govern every future decision on the featureOptimizing an offline metric with no online correlation
4Component / model selectiontechnical + selectSwitching cost is high (index format, embedding dim, framework)Lock-in to a dying option
5Build vs. buycompetitive + selectA credible vendor existsRebuilding a commodity, or buying an unexitable dependency
6Privacy & data protectiondomainAny personal data, profiling, or automated decisioning — see §13Redesign after legal review; regulatory exposure
7Security, abuse & adversarial MLtechnicalThe feature is user-facing, user-influenced, or model-driven — see §14Shipping a manipulable ranking surface
8Licensing & IPdomainAny third-party model, dataset, or copyleft library — see §17Discovering the model is non-commercial after integration
9Procurement & vendor assurancecompetitiveVendor will process your data — see §17Legal blocks a vendor you already built against
10Standards & interoptechnicalLanguage tags, encodings, collation, protocols, schemasSubtly wrong behavior in locales you don't test
11Accessibility & inclusive designtechnical / domainNew UI surface, especially multilingualWCAG failures found at audit; RTL and text-expansion breakage
12Fairness, bias & transparency dutiesacademic-lit + domainRanking or recommending to people at scalePopularity-bias spirals; unmet transparency obligations
13Algorithm & technique trendsacademic-litYou suspect the state of the art moved — see §15Building on a technique the field has quietly abandoned
14Failure-mode / postmortem researchtechnicalBefore designing anything with a known-hard failure modeRediscovering others' incidents in your own production
15Cost modeling at scaletechnicalPer-request inference, egress, or storage costs dominateUnit economics that only fail at volume
16Dependency health & maintenance risktechnicalAdopting a library or model you'll depend on for yearsAdopting an abandoned project with a bus factor of one
17Observability & eval toolingtechnical + selectYou need to measure a system you can't easily inspectNo way to tell whether the feature is working
18Migration & deprecation risktechnicalYour current stack component has a sunset signalForced migration on someone else's timeline

11.1 Underrated advantages of running research as a skill rather than ad-hoc chat

Beyond the findings themselves:

  • It's a durable team artifact. research.md with provenance frontmatter outlives the chat, the sprint, and the engineer. New joiners read why the decision was made instead of asking.
  • It's audit evidence. Publisher + publication date + access date on every claim is precisely the shape compliance functions want. In a regulated environment, "we evaluated these options against these criteria on this date, here are the sources" is materially valuable on its own.
  • It settles disagreements on evidence rather than seniority. A cited weighted matrix is harder to argue with than an opinion, and easier to change when the evidence changes.
  • It's negotiation leverage. A sourced pricing and feature comparison is a real input to a vendor conversation.
  • It parallelizes. Draft mode fires the expensive crawl into a subscription tool while you keep working; Process folds the result back in.
  • It's automatable. The headless JSON contract means a quarterly refresh can run in CI, not on someone remembering.
  • It inoculates against your own agentic pipelines. If you're building AI features, "no conclusions from training data" and the firewall are the disciplines you'll want in your own retrieval systems anyway. Deep Recon is a working reference implementation.
  • It forces the decision to be written down. Half the value arrives at the plan gate, before a single search runs, because you had to state what you're actually deciding and under what constraints.

12. Applied: a user recommendation feature

Much of this feature is internal work — but the external parts are not the ones most engineers expect. Privacy and security carry more research value here than model selection does.

12.1 Decisions that genuinely need external evidence

DecisionType + shapeDimensions to insist on at the plan gate
Embedding model for item/user vectorstechnical + selectMultilingual coverage, dimensionality vs. index cost, MTEB/BEIR retrieval numbers with dataset caveats, license, inference cost, self-host vs. API
Cross-encoder reranker vs. MMR-only diversitytechnical + selectLatency at your p99 budget, quality delta on public rerank benchmarks, serving cost, staleness of published numbers
ANN index choice / HNSW parameterization defaultstechnicalRecall-vs-latency curves published by index maintainers, memory footprint at your cardinality, filtered-search behavior
Offline metric conventionsacademic-litNDCG@k vs. MRR vs. Recall@k in the recsys literature, position-bias correction, offline↔online correlation evidence
Cold-start strategyacademic-lit or domainPublished approaches, reported lift, honest failure reports
What users complain about in "recommended for you" surfacesuser-voiceFilter-bubble complaints, repetition, staleness, opacity
Lawful basis, retention, erasure, transparency dutiesdomainSee §13 — the highest-value research on this feature
Manipulation, poisoning and inference attackstechnicalSee §14 — shilling, embedding inversion, membership inference
Popularity bias and fairness obligationsacademic-lit + domainMeasured popularity-bias effects, mitigation cost in NDCG, disclosure duties
Embedding model / reranker licensingdomainField-of-use restrictions, commercial-use thresholds, dataset provenance

12.2 Decisions that should not go to research

  • Your index mapping, shard layout, refresh interval — read your cluster.
  • Which features exist in your event stream — read your schema.
  • Bucketing/assignment for A/B — that's design, not evidence.
  • "How do I implement MMR" — you know this; it's twelve lines.

12.3 A worked plan-gate framing

Decision: pick the embedding model for the recommendation retrieval stage, deployable on our existing OpenSearch/HNSW infrastructure, for the next ~12 months.

Constraints: must handle our language mix; ≤200 ms p99 for the retrieval stage; self-hostable preferred; index memory budget is a hard ceiling.

Candidates to include (not to confirm): [3–5 named], plus any strong option the research surfaces.

Type: technical, select shape. Effort: deep. Validation: high.

Freshness bar: benchmark claims older than 12 months are reported as history.

Note the phrasing "to include, not to confirm." Naming candidates without that framing quietly asks the run to justify your shortlist.

12.4 What good output looks like

  • A weighted matrix with your constraints as the weights, not generic ones.
  • Every benchmark number carrying its dataset, date, and who ran it — vendor-run numbers flagged as such.
  • An explicit "what we could not determine" section (Process mode gap-checks for exactly this).
  • A staleness map that says, in effect: re-check the model landscape in 4 months; the license terms in 12.

13. Deep dive: privacy and regulatory research

Not legal advice. This produces an evidence base for a conversation with privacy counsel, not a substitute for one. The value is that you arrive at that conversation with cited primary sources and specific questions instead of vague anxiety.

13.1 Why this is the strongest case for Deep Recon

Privacy research satisfies every condition that makes the skill worth its cost, simultaneously:

  • Entirely external. Statutes, regulator guidance, enforcement decisions, case law. None of it is in your repo, and none of it is inferable from your architecture.
  • Jurisdiction-plural. A Toronto-built product with EU users sits under PIPEDA, Quebec Law 25, GDPR, and the DSA at once — and they don't agree on consent, notice, or automated decisioning.
  • Fast-moving in exactly the layer that matters. Statute text is stable; regulator guidance and enforcement posture are not, and it's the guidance that determines whether your design passes.
  • Load-bearing and expensive to reverse. You can swap an embedding model next sprint. You cannot swap a lawful basis after you've collected two years of data under it.
  • The worst possible case for model memory. Legal reform moves constantly and a confidently-stated stale claim here is far more damaging than a stale benchmark. The no conclusions from training data rule is doing real work.

That last point is not hypothetical. Canadian federal privacy and AI reform has been through multiple bill cycles; I would not assert its current status from memory, and neither should any model you're relying on. That uncertainty is the argument for running the research, not a reason to skip it.

13.2 Split it into two engagements, not one

The common mistake is one sprawling "GDPR for recommender systems" run. That's two different decisions with two different source policies, and merging them produces a report that's shallow on both.

Run A — domain type: what the rules actually require

Dimensions to insist on at the plan gate:

  • Lawful basis for profiling-based recommendation. Consent vs. legitimate interest, and specifically the contexts where legitimate interest has been rejected by regulators. Include the balancing-test documentation expectations.
  • GDPR Art. 22 threshold. When does ranking become "automated decision-making producing legal or similarly significant effects"? Ordinary content ranking usually doesn't; ranking that gates access, pricing, or opportunity may.
  • Art. 15 access and Art. 20 portability. What must you be able to show a user about their own profile? This has direct schema consequences.
  • Art. 17 erasure scope. Does the right reach a trained model and derived embeddings, or only the raw interaction log? Regulator positions differ and have moved.
  • Purpose limitation and secondary use. Interaction data collected for one surface being reused to train a ranker for another is a live issue.
  • DSA recommender-system obligations. Parameter transparency, and the requirement to offer a non-profiling option — plus the thresholds that determine whether they apply to you at all.
  • EU AI Act. Recommenders are generally not Annex III high-risk, but transparency duties and GPAI obligations can still attach if you're using a foundation model in the pipeline. Get the classification question answered explicitly rather than assumed.
  • Quebec Law 25 automated-decision notice. Stricter than PIPEDA on this point and routinely missed by teams who "did GDPR."
  • PIPEDA and Canadian reform status. Current, not remembered.
  • US state law patchwork. Where sensitive-inference and opt-out-of-profiling provisions bite.
  • Children's and minors' data. Separate, stricter, and often triggers age-assurance obligations.
  • Cross-border transfer posture. Applies to embedding inference calls, which teams frequently forget are transfers.

Run B — technical type: how to actually satisfy the above

  • Right-to-erasure vs. HNSW reality. Vector indexes don't truly delete — you get a soft delete plus eventual segment merge. What's the real deletion latency, is it bounded, and does a bounded-but-slow deletion satisfy the requirement? This is a genuine engineering-meets-law question with published discussion.
  • Are user embeddings personal data? Increasingly treated as yes. If so, every derived vector inherits the obligations.
  • Embedding inversion attacks. Published work shows embeddings can leak the attributes they were built from — which converts a "we only store vectors" argument into a liability.
  • Membership inference. Can an attacker determine whether a given user was in the training set?
  • Machine unlearning. What's genuinely deployable versus what's a paper with a toy benchmark. Be ruthless here.
  • Differential privacy / cohort aggregation. What does the privacy budget actually cost you in NDCG?
  • Retention windows. How much interaction history does the model measurably need, versus how much you're keeping by default? Data minimization is easiest to argue when you've measured the marginal value of older data.
  • Consent propagation. When a user withdraws consent, what happens to precomputed recommendation caches, materialized candidate sets, and warm segments?
  • Pseudonymization that survives joins. Whether your scheme actually resists re-identification given the other datasets you hold.

13.3 Configuration that matters for regulatory research

KnobSettingWhy
Source policy — preferredEUR-Lex, EDPB, national DPAs, ICO, OPC Canada, CAI Québec, court decisionsPrimary texts and regulator guidance beat everything
Source policy — bannedLaw-firm marketing blogs, compliance-vendor content marketingThis space is saturated with SEO'd summaries that misstate scope and are optimized to sell a product
validationhigh minimumA wrong regulatory claim is load-bearing by definition
red_teamon (it's off by default)If you walked in believing you're compliant, buy the adversarial pass
Freshness windowsStatute ~36mo · guidance ~12mo · enforcement/case law ~6moThese claim classes genuinely age at different rates
FirewallDo not paste your retention design inOtherwise you get a well-cited report explaining why your current design is fine

The firewall discipline is at its most valuable and most tempting to break here. The instinct to say "here's how we currently handle deletion, is it compliant?" produces a motivated report. Ask "what does the law require for deletion in vector stores?" and compare afterward.

13.4 What to hand counsel

The output should let a lawyer work in an hour rather than a week:

  • Requirements traced to primary sources, with dates, not to secondary summaries
  • An explicit "could not determine" section — jurisdictional gaps are the most important finding
  • Conflicts between regimes surfaced as conflicts, not averaged away
  • A staleness map that says which obligations to re-check and when
  • Your specific open questions, framed as questions

Then keep it refreshed. The "overturned claim feeds a downstream artifact" warning is at its most valuable here: if a lawful-basis assumption in your PRD stops holding, you want the delta report to tell you, not an auditor.


14. Deep dive: security and adversarial research

Security is the other category I'd rank above component selection, and it's the one most consistently skipped until a review board forces it.

14.1 Why it fits the skill

Threat knowledge is published, adversarial, and perishable — the three properties the verification and freshness machinery exist for. It's also a domain where the model's training data is systematically incomplete: novel attack classes appear faster than they get absorbed.

14.2 Threat dimensions for a recommendation feature

Most engineers threat-model the service (authn, injection, rate limits) and skip the ranking system as an attack surface. Both belong in the run.

Manipulation and integrity

  • Shilling / profile-injection attacks — coordinated fake interaction patterns that promote or bury items. Published attack models and detection approaches exist; this is a solved-ish literature that most teams have never read.
  • Data poisoning of the training or feedback loop, including slow-drip attacks below anomaly thresholds
  • Popularity and trending manipulation, and feedback-loop amplification
  • Catalog enumeration via recommendation responses

Confidentiality and inference

  • Embedding inversion — recovering source attributes from a vector (overlaps §13, but it's a security control question here)
  • Membership inference — determining whether a user or item was in training data
  • Cross-user leakage — inferring another user's behavior from changes in shared segments, co-occurrence surfaces, or "people also viewed"
  • Model extraction — reconstructing your ranking function through systematic querying
  • Cache and shared-segment poisoning

Access control

  • IDOR and horizontal privilege issues on personalization endpoints — mundane, and the most commonly exploited

14.3 If there's an LLM anywhere in the pipeline

Add these dimensions explicitly:

  • Indirect prompt injection via user-generated content, item metadata, or retrieved documents — the dominant risk class for retrieval-backed systems
  • Tool-use boundary design and least-privilege for agentic components
  • Output handling — treating model output as untrusted before it reaches a shell, a template, a query, or a browser

PR #2611 is itself the canonical worked example: a research brief was being interpolated into a double-quoted shell template, so $(), backticks, or a quote break inside researched or imported content could shape a command — amplified by a permission-skipping flag. The fix was structural, not a filter: briefs are written to a file and the template substitutes a skill-generated path, so untrusted text never touches a command line. Take that pattern wholesale into your own agent tooling. Filters on untrusted text fail; changing the channel so the text never reaches an interpreter doesn't.

14.4 Supply chain

  • Model provenance — where the weights came from, what's actually verifiable
  • Serialization risk in model artifacts (pickle-family deserialization remains a live RCE vector)
  • Dataset provenance and whether training data carries obligations
  • Dependency health — release cadence, maintainer count, open-issue and security-advisory trends
  • CVE landscape for your serving stack, index, and inference runtime

14.5 Configuration for security research

KnobSetting
Preferred sourcesNIST, OWASP, MITRE ATT&CK/ATLAS, CVE/NVD, vendor security advisories, peer-reviewed adversarial-ML venues, credible incident writeups
Banned sourcesSecurity-vendor content marketing, "top 10 threats" listicles
validationhigh
red_teamon — this is the category it was built for
FreshnessAttack classes ~24mo · advisories ~3mo · tooling ~6mo

14.6 Turning the report into work

The output shouldn't stop at a threat list. Convert it: threat → likelihood/impact for your deployment → control → story. Threats you consciously accept get written down as accepted, with the citation attached. That record is what makes the next security review fast, and it's the thing that's always missing when a reviewer asks "did you consider X?"


15. Deep dive: algorithm and technique-trend research

This is the weakest of the categories you raised — but it's rescuable if you reframe it.

15.1 First, disambiguate "EMA"

Two unrelated research problems share the acronym:

  1. Exponential decay of user interest profiles — recency-weighted aggregation of a user's interaction vectors, so last week outweighs last year. A ranking-features question.
  2. EMA teacher weights in self-supervised training — momentum encoders in the BYOL/DINO/MoCo family, where a slowly-updated copy of the network supervises the fast one. A representation-learning question.

They share nothing except the smoothing formula. A run that doesn't disambiguate produces a report that's half-relevant twice over.

No decision means no dimension pruning, which means unbounded scope, maximum fan-out, and an expensive encyclopedia entry you'll skim once. Convert it into a decision:

Vague askDecision framingType + shape
"Trends in EMA" (sense 1)"Replace fixed-window last-N profile aggregation with EMA decay — and what half-life per surface?"technical + select
"Trends in EMA" (sense 2)"Has EMA-teacher SSL held up against contrastive training for retrieval embeddings at our scale and data volume?"academic-lit
"What's new in reranking""Is a cross-encoder rerank worth 40ms at our p99, versus MMR-only?"technical + select

15.3 The dimension you must add by hand

Negative results, failed reproductions, and industry postmortems. Name it explicitly at the plan gate.

The literature is systematically positive-biased: papers report wins, and a survey that only aggregates claimed improvements is worse than nothing — it's confidently wrong in a specific, actionable direction. Recommender systems have a well-documented reproducibility problem in particular, where reported gains over properly-tuned simple baselines often fail to replicate.

This is also where "no conclusions from training data" pays off most, because model memory of an ML technique is weighted heavily toward the abstract of the original paper — the most optimistic sentence anyone ever wrote about it.

15.4 Read benchmark claims like an adversary

Require every reported number to carry:

  • The dataset and its known idiosyncrasies
  • The date — a 2023 retrieval benchmark describes a world that no longer exists
  • Who ran it — self-reported vendor numbers flagged as such
  • Tuning-budget parity — was the baseline tuned as hard as the proposed method? Usually not.
  • Whether the gain survives at your scale and data regime, which is rarely the paper's regime

15.5 Know where research stops and your eval starts

For a parameter like an EMA half-life, research gives you the prior and the conventions — plausible ranges, how others parameterize per surface, what failure looks like when decay is too aggressive. It cannot give you the answer. Your offline replay against your own interaction logs decides it, and your A/B confirms it.

Running a deep engagement to pick a number your own data can answer in an afternoon is the clearest form of research-as-procrastination. Use quick for the prior, then go measure.


16. Applied: a multilingual translation feature

This is the higher-value case, because almost every decision is external.

16.1 Decision map

DecisionType + shapeWhy external
MT provider / model selectioncompetitive + selectPricing, language pairs, quality claims, and rate limits are all public and all move
Quality evaluation methodologyacademic-litBLEU vs. chrF vs. COMET vs. LLM-as-judge is a live methodological debate; picking wrong invalidates your whole eval
Locale, normalization, and collation handlingtechnical or domainThese are external standards (Unicode/CLDR/ICU), not opinions
Human-in-the-loop / post-editing workflowdomain + user-voiceLocalization industry has established practice you shouldn't reinvent
Data residency and PII in translation payloadsdomainRegulatory, jurisdiction-specific, and changes
Terminology/glossary and do-not-translate handlingtechnicalProvider feature matrices differ sharply here and are poorly summarized

16.2 Traps this pack is built to catch

  • Vendor-reported quality numbers. The source-quality card treats an aggregator as one publisher; verification at high cross-checks the claim classes the recommendation rests on. Insist that self-reported quality claims are marked as such.
  • Language-coverage asymmetry. "Supports 100+ languages" is a marketing claim; per-pair quality varies by an order of magnitude. Make coverage-quality a named dimension, not a checkbox.
  • Stale benchmarks. Set the freshness bar aggressively — 12 months, not 36.
  • Pricing shape, not just price. Per-character vs. per-token vs. per-request vs. committed-use changes the ranking entirely at your volume. State your actual volume at the plan gate.

16.3 Suggested mode

Draft → Process → focused Run on the gaps:

  1. Draft a prompt for whichever deep-research product you subscribe to. Vendor landscapes are exactly what hosted crawlers are good at.
  2. Run it there; bring the report back.
  3. Process it — original preserved in imports/, claims extracted, gaps flagged against the competitive pack.
  4. Run natively on the two or three gaps (usually: your specific language pairs, and anything requiring internal or MCP-only sources).

16.4 Then refresh

Set a calendar reminder from the staleness map. MT pricing and model versions turn over on a roughly quarterly cadence. refresh the translation vendor research gives you a delta report — confirmed / changed / overturned — with a warning if an overturned claim underpins something you already shipped a decision on.


17. Deep dive: licensing, procurement and vendor risk

The most-skipped category, and the one that kills projects latest in the cycle — after integration, when unwinding is most expensive.

17.1 Model and dataset licensing

  • Open weights ≠ open source. Many popular model licenses carry field-of-use restrictions, acceptable-use policies, monthly-active-user thresholds that flip commercial terms, or naming and attribution requirements. Several forbid using outputs to train competing models — which matters if you plan to distill.
  • Dataset provenance. A permissively-licensed model trained on a non-commercial dataset is a real and common trap, particularly for multilingual and speech models.
  • OSS license compatibility with how you actually distribute — SaaS, on-prem, embedded, and client-side each trigger different obligations.
  • Output IP and indemnification. Who owns generated translations? Does the vendor indemnify you against third-party claims, and under what conditions do they void it?

Type: domain (or technical when it's mostly library compatibility). Preferred sources: the license texts themselves, model cards, official FAQs. Banned: summaries of licenses. Read the primary text; this is a category where paraphrase loses the operative clause.

17.2 Procurement and vendor assurance

If a vendor will process your data — an MT API, a hosted embedding endpoint — these determine whether you're allowed to use them at all, and they should be researched before you build against the API, not after:

  • SOC 2 Type II / ISO 27001 status and scope
  • DPA availability, SCCs, and transfer mechanism
  • Sub-processor list and whether it's stable and notified
  • Data residency and processing-location commitments
  • Training-on-customer-data policy — the default matters, and so does whether opting out changes pricing or features
  • Retention and deletion commitments, with actual timelines
  • Breach notification SLA
  • Exit terms and data portability — the exit cost is the real measure of lock-in
  • Uptime SLA, rate limits, and what happens at the ceiling

Type: competitive. This is the research whose absence produces the sentence "legal won't approve the vendor we've already integrated."

17.3 Dependency and deprecation risk

Cheap to run, disproportionately useful:

  • Release cadence and whether it's slowing
  • Maintainer count and bus factor
  • Open-issue and security-advisory trend lines
  • Corporate backing, and whether the backer has a history of sunsetting
  • Migration paths that already exist for people leaving

A quick preset run answers most of this and has saved more projects than any model comparison.


18. Best practices

18.1 Framing

  1. Lead with the decision, not the topic. "Research embeddings" gets you an encyclopedia entry. "Pick the embedding model for our recommendation retrieval stage under these constraints" gets you a recommendation.
  2. State your constraints as constraints. Latency budget, memory ceiling, language mix, volume, license posture, deploy target. These become matrix weights.
  3. Name candidates as inclusions, never as the frame. Otherwise you get a confirmation exercise.
  4. Declare your freshness bar when the field moves fast. Don't accept the pack default silently.
  5. Pick the decision shape deliberately. select for choose-between, explore when you don't yet know the option space. Running select too early narrows prematurely.

18.2 At the plan gate

  1. Read every dimension. This is your only hard stop. Delete dimensions that don't bear on the decision — each one costs a full fan-out.
  2. Sanity-check the topology. Independent sub-questions → breadth. One contested question → depth. A lookup → straightforward. If it proposes six assistants for something simple, say so.
  3. Match effort to reversibility. quick for a decision you can undo next sprint; deep + validation = high for something that becomes load-bearing.
  4. Check the time estimate against your patience. Approving a 40-minute run you'll abandon at minute 12 wastes everything except what's already on disk.

18.3 During and after

  1. Trust the firewall; don't defeat it. Resist the urge to paste your architecture doc "for context." Project context is legitimately framing; it is inadmissible as evidence. Feeding it in is how you get a report that agrees with you.
  2. Chase answer-engine citations to primaries. If the report leans on an aggregator, that's one publisher, not three.
  3. Read the "could not determine" section first. Gaps are more informative than confirmations.
  4. Check the staleness map and act on it. Research that's never refreshed is research that silently becomes wrong.
  5. Keep research.md in version control alongside the PRD it grounds. The provenance frontmatter is what makes the decision auditable in six months.

18.4 Cost and token discipline

  1. Draft → Process is the cheap path. If you already pay for a deep-research subscription, the round-trip is one paste and it crawls wider per dollar.
  2. Default to standard. The presets were rescaled downward because v1 was too slow and too token-heavy. Reach for deep deliberately.
  3. Use deepen instead of re-running. Drilling one dimension is far cheaper than a fresh engagement.
  4. Use refresh instead of redoing. It re-verifies only what's stale.

18.5 Hygiene

  1. Turn on red_team for decisions with a strong prior. It's off by default. If you walked in already believing the answer, buy the adversarial pass.
  2. Redact before quoting people. The user-voice pack now requires stripping usernames, handles, emails, and identifying URLs from verbatim quotes — carry that rule into anything you paste into a PRD.
  3. Never shell-interpolate researched text. The PR's critical finding. Researched and imported content is untrusted input; pass it by file path, not on a command line — especially near any --dangerously-skip-permissions flag.

18.6 Domain-specific craft

  1. Run the compliance and security gates in week one, not week ten. Their findings are design constraints. Discovered late, they're rewrites.
  2. Tune source policy per domain, not once. Regulatory research wants regulators and bans law-firm blogs; security research wants NIST/OWASP/CVE and bans vendor marketing; academic research wants venues and bans press releases. The default policy is a compromise; override it.
  3. Set freshness per claim class, not per run. Statute text, regulator guidance, and enforcement posture age at wildly different rates — and so do attack classes, advisories, and tooling.
  4. Always add a "negative results and failures" dimension to any technique or trend research. Published literature is positive-biased and won't volunteer it.
  5. Split regulatory research into requirements and implementation. One run answers "what must be true"; a second answers "how do we make it true in a vector index." Merging them makes both shallow.
  6. Demand a "could not determine" section on compliance work. In regulatory research the gaps are the finding — an unresolved jurisdictional question is more actionable than five confirmed ones.
  7. Convert security output into accepted-or-mitigated decisions, with citations attached. A threat list is not a deliverable; a threat register with dispositions is.
  8. Know where research stops. Priors and conventions come from the literature; the actual parameter comes from your offline replay. Research that could be settled by an afternoon of measurement is procrastination.

19. Anti-patterns

Anti-patternWhy it failsDo instead
"Research recommendation systems"No decision → unbounded dimensions → expensive encyclopediaName the decision and constraints
Running research on your own codebaseFirewall means it looks outward; your repo isn't a sourcebmad-document-project
Pasting architecture docs "for context"Defeats the firewall; produces agreeable findingsLet context shape questions only
deep + max on every runBurns tokens and minutes for marginal gainScale to reversibility
Approving the plan gate without readingThe one place your judgment is irreplaceablePrune dimensions, check topology
Treating research.md as a one-time artifactClaims rot; downstream artifacts inherit the rotRefresh from the staleness map
Using research to decide how to implementWrong tool; you need your codebase, not the webStory workflows
Accepting vendor-published quality numbersSystematically flatteringFlag self-reported; verify at high
Naming your preferred option in the framingConverts research into justification"Include X, Y, Z — and anything better"
"Is our current design GDPR-compliant?"Motivated framing; the firewall exists to prevent exactly this"What does the law require here?" — then compare yourself
One giant "GDPR + security + vendors" runThree source policies in conflict; shallow on all threeThree engagements, three policies
Sourcing compliance claims from law-firm blogsSEO'd, scope-distorting, sales-motivatedBan them; require regulators and primary texts
Threat list with no dispositionsReads like diligence, changes nothingThreat → likelihood/impact → control or accepted, cited
Trend research with no negative-results dimensionAggregates only claimed wins; confidently wrongName failed reproductions as a required dimension
Researching a parameter your own data can settleExpensive procrastinationquick for the prior, then measure
Vendor research after integrationDiscovers blockers when unwinding costs mostProcurement gate before the first API call
Assuming open weights means usableField-of-use and MAU clauses bite lateRead the license text, not a summary

20. Command cheat sheet

GoalType this
Start research/bmad-deep-recon, then describe the decision
Force a type"competitive research on DeepL and Google Translate"
Draft for your own tool"draft a deep research prompt about X for Gemini"
Process a report"there's a research report at ~/Downloads/report.pdf, process it"
Choose between options"help me choose between Postgres and MySQL for this"
Refresh"refresh the market research"
Deepen one dimension"deepen the pricing dimension"
Customize defaults/bmad-customize bmad-deep-recon

Legacy IDs (bmad-market-research, bmad-domain-research, bmad-technical-research) still work and forward with the type pre-set.


21. Transferable design lessons

Independent of whether you adopt Deep Recon, these generalize to any agentic system you build:

  1. Consolidate near-duplicate prompt trees into one skill with data-driven variation. 5,136 lines → ~650 by turning three workflows into six ~25-line policy cards.
  2. Keep the entry point lean; defer detail. ~4k → ~2.1k tokens by carving procedures into just-in-time reference files. Entry-point tokens are paid on every invocation.
  3. Files-first, conversation-as-control-channel. Anything valuable goes to disk on arrival. Contexts die; disks don't.
  4. Verify at landing, not at the end. A late pass degraded quality in field testing.
  5. Mechanical work belongs in a tested script. Counting, cross-referencing, date math, slug generation.
  6. Build an explicit bias firewall. Separate "what shapes the question" from "what counts as evidence," and enforce it at the subagent boundary.
  7. Choose fan-out topology deliberately. Parallelism is a cost, not a virtue.
  8. Treat tool output as untrusted input. File-based handoff over shell interpolation, protocol allowlists, escaping at the boundary.
  9. Give artifacts a lifecycle. Staleness maps, delta refreshes, and downstream-impact warnings turn a document into an asset.
  10. Single-writer rule for parallel agents. Fan out reads; serialize writes through the lead.

22. The epistemology underneath all of this

This section assumes no philosophy background. Every term is defined in plain language before it's used.

The PR has a section literally headed "Epistemics and reliability." That's not decoration — it's the honest label for what the whole design is. Read it this way and the mechanisms stop looking like a feature list and start looking like a single coherent argument.

22.1 What "epistemology" means

Epistemology is the branch of philosophy that asks: what is knowledge, and what makes a belief justified?

The classic starting point: knowing something requires three things.

  1. It's true
  2. You believe it
  3. You have good reasonjustification — for believing it

Miss any one and you don't have knowledge. If it's true but you have no grounds, you got lucky. If you have grounds but it's false, you're mistaken.

Why this matters here: an LLM produces sentences that satisfy #1 and #2 by accident and skip #3 entirely. It asserts things without holding beliefs, and its confidence is uncorrelated with whether it has grounds. It can tell you something perfectly true while having no justification whatsoever — the sentence just came out fluent.

There's a name for the case where you have a true, believed claim that's only accidentally right: a Gettier case (after a famous three-page paper from 1963). Picture a stopped clock that happens to show the correct time when you glance at it. You now believe something true, for a reason that gives you no actual grounds. Every uncited model output is a potential stopped clock.

Deep Recon's whole purpose is to bolt a justification layer onto a system that natively has none.

22.2 The core concepts, defined

ConceptPlain meaningEveryday example
JustificationThe grounds you have for a belief — the why behind it"The bridge is closed" vs. "The bridge is closed because I just drove past the barrier"
TestimonyBelieving something because someone told you. Most of what anyone knows works this way — you've never personally verified that Antarctica existsTrusting a doctor, a map, a news report
FoundationalismThe view that beliefs form a structure resting on a base — some things are believed because of other things, and the chain has to stop somewhere solidA wall of bricks needs a foundation, not infinitely more bricks below
DefeasibleA belief held provisionally, which new evidence can overturn. It's "reasonable until defeated," not "proven forever""The train runs at 8" — until they change the timetable
DefeaterThe new evidence that overturns itThe posted schedule change
FallibilismAccepting that any of your beliefs might be wrong, without concluding that nothing is worth believing. Confidence without certaintyScience's default posture
FalsificationTesting an idea by trying hard to break it rather than piling up examples that fit. Associated with Karl PopperDon't count white swans; go looking for a black one
CalibrationYour confidence matching your actual accuracy. Being 70% sure of things that turn out true about 70% of the timeA weather forecaster who says "30% rain" and is right about that often
ProvenanceWhere a claim came from, and through whose handsA museum tracing an artifact's ownership chain
IndependenceTwo sources counting as genuinely separate evidence, rather than both repeating one originalTwo newspapers citing the same wire story are one source, not two

22.3 Every mechanism is an epistemological commitment

Now the mapping. Each row is a design choice in the skill and the philosophical position it encodes.

MechanismWhat it commits to
"No conclusions from training data"Truth alone isn't enough — a claim needs traceable justification. This is the rule that bars the stopped clock.
Inline [n] → source appendixFoundationalism. Every claim has to bottom out in a retrieved source, not in another claim. No infinite regress, no circles.
"Answer engines count as one publisher — chase their citations"Guards against false independence. Three sites echoing one primary report are one witness wearing three hats. Counting them as three is how confident nonsense propagates.
Publisher + publication date + access date on every claimThe epistemology of testimony. Since you're believing things you didn't verify yourself, you need to know who said it, when, and when you looked — that's exactly what lets a reader evaluate it.
Preferred / banned source policyA reductionist stance on testimony: trust is earned, not presumed. Publishers are ranked. "Someone published it" is not a credential.
Claim statuses (unverified → verified → disputed → overturned)Defeasible reasoning made explicit. Beliefs are held provisionally with their status visible, rather than silently promoted to fact.
"An outcome adjusts a claim's status — it never licenses rewriting findings"Defeat must stay visible. You don't quietly delete the belief you used to hold; you mark it overturned. Erasing the old belief destroys the audit trail that made the new one trustworthy.
Staleness map + freshness windows per claim classFallibilism with a clock. Different kinds of claims decay at different rates — statute text ages slowly, pricing fast. Justification has a shelf life.
"A three-year-old market size is history, not fact"A subtle one: the proposition itself changes character over time. It stops being a claim about the present and becomes a claim about the past.
Red-team passFalsification. Attack the conclusion instead of accumulating agreeable evidence. Off by default, which is a cost decision, not an epistemic one — turn it on when you had a prior.
"Could not determine" sectionCalibration. Reporting the shape of your ignorance. Most systems — and most people — never do this, which is why overconfidence is the default failure.
Negative-results dimension for literatureCorrects publication bias: the evidence base itself is distorted, because journals print wins and quietly drop failures. Surveying only what got published systematically overestimates.
Verification at landing, not at the endJustification is checked while the context that produced it still exists. A late pass evaluates claims stripped of their situation — and field testing showed it degraded quality.
Files-first / memlog as truthThe record of how you came to believe something must outlive the conversation. Reasoning that evaporates can't be audited.

22.4 The elegant one: the research firewall

This is the deepest idea in the design, and it's a direct implementation of a distinction from philosophy of science.

Context of discovery vs. context of justification (Hans Reichenbach, 1930s). These are two separate questions about any belief:

  • Discovery: how did you come up with this idea? A hunch, a dream, a colleague's offhand remark, your existing assumptions — anything goes. There's no wrong way to generate a hypothesis.
  • Justification: what makes it true? Here almost nothing goes. The grounds have to be independent of how you happened to think of it.

The classic illustration: the chemist Kekulé reportedly hit on the ring structure of benzene after daydreaming about a snake biting its tail. Perfectly fine as discovery. Utterly irrelevant as justification — the structure is right because of the evidence, not because of the dream.

The firewall implements exactly this split at a process boundary:

  • Your project files, architecture docs, and briefs are allowed to shape which questions get asked — that's discovery, and your priors are a legitimate source of good questions.
  • They are inadmissible as evidence — that's justification, and it has to come from outside.
  • Subagents receive only their assignment. persistent_facts defaults to empty.

Why this is unusually good engineering: most bias mitigation is an instruction — "be objective," "consider alternatives." Instructions fail, because a system that wants to agree with you will find a way. This is architectural. The subagent cannot be biased by context it was never given.

The failure it prevents is the single most common one in agentic research: hand an agent your design doc and ask it to research alternatives, and it comes back explaining that your design is excellent. Not through deceit — through ordinary confirmation bias, which is what you get when the thing generating hypotheses also gets to grade them.

22.5 Where the epistemology is genuinely weak

Being honest about this matters more than admiring the design.

  1. The independence assumption is shaky. Fan-out to multiple assistants borrows its logic from a result about crowds: if many judges each do slightly better than chance and their errors are independent, the majority is very reliable. But these subagents share a base model, a training corpus, and query craft. They are correlated witnesses, not independent ones. Six of them do not buy six times the reliability — they buy the same blind spot, six times, expressed differently.

  2. Verification isn't independent either. The system checking a claim is the same system that produced it. That's a coherence check — does this hang together? — not corroboration, which requires a genuinely separate source. Useful, but weaker than it looks.

  3. The web is not a neutral evidence base. Search ranking is itself an epistemic filter, shaped by commercial incentives. What is findable is not the same as what is true, and the gap is systematic rather than random. SEO-saturated domains — compliance, security vendors, "best X for Y" — are exactly where this bites hardest, which is why the banned-source policy is doing far more work than it appears to.

  4. Citation prevents fabrication, not laundering. A well-cited report built on weak sources is more dangerous than an uncited one, because it wears the costume of justification. Rigor in the form can disguise rot in the content.

  5. The plan gate is the least-examined step and the most consequential. You choose which dimensions count before any evidence exists. Every downstream mechanism assumes the question was well-posed. That's where the deepest bias enters — and nothing in the system checks it. Which is precisely why it's the one place your own judgment is irreplaceable, and why "read every dimension at the plan gate" is the most important practice in §18.

The honest summary: Deep Recon doesn't give you knowledge. It gives you auditable justification — a chain you can inspect and challenge. That's a real and unusual improvement over uncited fluency, and it's less than it can look like when the output is well-formatted and thoroughly cited.

22.6 Why this matters doubly for search and recommendation work

Here's the reflexive turn: search and recommendation systems are themselves epistemic technologies.

A ranking function is an implicit theory of relevance. A theory of relevance is an implicit theory of what is worth knowing. When you build a recommender, you are building a machine that shapes what people can come to know — you're operating on the justification side of other people's beliefs, at scale.

Which means the same concepts run in both directions:

Epistemology conceptIts form in your systems
Publication biasPopularity bias — rich-get-richer feedback loops distorting what's available to be seen
Epistemic closure / narrowed inquiryFilter bubbles — the system narrowing the space of what a user can encounter
Transparency of groundsDSA recommender-transparency duties — a legal demand that an epistemic intermediary disclose its criteria
Construct validity (does the measure capture the thing?)Offline/online metric divergence — NDCG moves, engagement doesn't, because the proxy measures something adjacent
Testimony and provenanceSource attribution in retrieval and RAG — whether users can trace what they're being shown
Independence of sourcesDeduplication and diversity in candidate sets — ten versions of one document are one document
CalibrationConfidence display and abstention — showing "we're not sure" instead of always returning ten results
The firewallPersonalization vs. evidence — a user's history should shape what you show them, not what you claim is true

That last row is the sharpest one, and it's worth sitting with. Deep Recon's firewall says: your context shapes the questions, never the answers. A recommendation system that lets a user's history shape what it presents as true — rather than merely what it surfaces — is doing the thing the firewall exists to prevent, to someone else, without their knowledge.

The discipline Deep Recon applies to your own knowing is the same discipline you owe your users about theirs. Provenance, defeasibility, calibrated uncertainty, source independence, visible criteria. In your research process they're hygiene. In your ranking systems they're ethics.


23. Worked walkthroughs for software engineers

Fidelity note. The session excerpts below are illustrative reconstructions. They show the shape of each interaction — the decision points, what you type, where you push back, what lands on disk. Exact prompt wording and gate formatting live in the skill's reference files and will differ in detail. The workflow, artifacts, and decision points reflect the documented design; the TOML key paths should be confirmed against your installed customize.toml.

23.0 Setup, once

npx bmad-method install          # select your IDE; deep-recon ships in core

Confirm it's there, then look at what you can change before your first real run:

/bmad-customize bmad-deep-recon

Two things worth setting on day one, before you've formed habits:

# _bmad/custom/bmad-deep-recon.user.toml   (personal, gitignored)

[workflow]
output_format = "both"      # md for the repo, html for sharing with non-engineers
validation    = "normal"    # raise per-run when the decision is load-bearing

Leave red_team off globally. Turn it on per-run, deliberately, when you notice you already believe the answer.


Walkthrough A — Native Run: choosing an embedding model

Scenario. You own multilingual search and recommendation. Retrieval runs on OpenSearch with HNSW. You need to commit to an embedding model for the recommendation retrieval stage and live with it for roughly a year, because re-embedding the corpus and rebuilding the index is a multi-week project.

This is the gate everyone runs. It's genuinely the least valuable of the gates — but it's the best one to learn the mechanics on, because the feedback loop is short.

A.1 The invocation, and the fork in the road

> /bmad-deep-recon

Deep Recon opens the floor and, on a bare research ask, states the Draft-vs-Run trade once and remembers your answer for the session. For this one, Run is right: you'll want follow-up rounds on contradictions, and some sources are technical enough that you'll want to chase citations interactively.

Now the part that determines everything downstream.

❌ What most engineers type:

research the best embedding models for multilingual search

What comes back: a competent encyclopedia entry. Every dimension is in scope because nothing was pruned. Maximum fan-out, maximum tokens, and a report that ranks models against generic criteria you don't share. You'll skim it once and decide on vibes anyway.

✅ What to type instead:

I need to pick an embedding model for the retrieval stage of our
recommendation system. Decision horizon ~12 months — re-embedding the
corpus is a multi-week project, so this is expensive to reverse.

Constraints:
- Content is multilingual; the mix is roughly [your actual mix]
- Retrieval stage budget is 200ms p99, end to end
- Index memory has a hard ceiling; dimensionality is a real cost, not a
  preference
- Self-hostable strongly preferred; API-only is a significant negative
- Must be usable commercially without a revenue-threshold clause

Include these candidates — but do not treat the list as the frame, and
surface anything stronger: [3-5 named models].

Type: technical, select shape. Effort: deep. Validation: high.
Freshness: benchmark claims older than 12 months are reported as history,
not as current fact.

Four things are doing work there:

ElementWhy it matters
The decision and its horizonPrunes dimensions. "12 months, expensive to reverse" tells the run to weight license stability and maintenance health, not just today's benchmark
Constraints as constraintsThese become the weights in the selection matrix. Without them you get generic weights
"Do not treat the list as the frame"Without this, naming candidates quietly converts research into justification for your shortlist
Explicit freshness barEmbedding benchmarks from 2024 describe a different world. The pack default may be more generous than you want

A.2 The plan gate — the only place your attention is irreplaceable

An illustrative gate for the above:

DECISION
  Select an embedding model for recommendation retrieval; 12-month horizon,
  high reversal cost.

DIMENSIONS (6)
  1. Multilingual coverage and per-language retrieval quality
  2. Dimensionality vs. index memory and HNSW build/query cost
  3. Published retrieval benchmarks (MTEB/BEIR family) with dataset caveats
  4. Inference latency and throughput; self-host vs. hosted
  5. License terms, commercial-use restrictions, model provenance
  6. Maintenance signals: release cadence, backing, deprecation risk

TOPOLOGY   breadth-first (dimensions are largely independent)
EFFORT     deep — 6 assistants / 12 sources per round / 3 rounds
VALIDATION high; red_team off
FRESHNESS  benchmarks 12mo · license 24mo · maintenance 6mo
ESTIMATE   ~35-45 minutes

Approve, or tell me what to change.

What to actually do here — this is the whole skill:

  1. Delete dimensions that don't bear on the decision. Each one costs a full fan-out. If you'll self-host regardless, dimension 4 collapses to "self-host latency only" — say so. Cutting one dimension at deep saves roughly 15% of the run.
  2. Add what's missing. Two things are absent above and belong: fine-tuning and adaptation cost (if you'll adapt to your domain, a model that resists adaptation is disqualified regardless of zero-shot numbers), and how each model behaves under the filtered-search patterns your recommender actually uses. Benchmarks measure unfiltered top-k; your production path is filtered.
  3. Sanity-check the topology. Breadth-first is right here — these are independent questions. If it had proposed depth-first, that would be a signal the decision was framed as one contested question rather than six separable ones.
  4. Check the estimate against your patience. 40 minutes is real. If you'll wander off at minute 12, drop to standard now. Files-first means you keep what landed, but you'll have a partial report you don't trust.

A realistic reply:

Approve with changes:
- Drop dimension 4's hosted-API sub-question; we self-host, full stop.
- Add: fine-tuning/adaptation cost and whether the license permits it
- Add: behavior under filtered ANN search, not just unfiltered top-k
- Keep effort at deep

A.3 During the run

You'll see digests landing and report sections committing per dimension, because everything hits disk on arrival. Two things to watch:

  • Aggregator pileup. If several digests trace back to the same summary site, that's one publisher wearing several hats. Say so: "dimension 3 is leaning on aggregators — chase the primary benchmark repos."
  • Round-two assignments. Contradictions from round one become round two's work. If two sources disagree on a latency figure and round two isn't chasing it, flag it. Disagreement is the highest-value lead in the run.

A.4 What lands on disk

_bmad/planning/embedding-model-selection-2026-07/
├── briefs/
│   ├── multilingual-coverage.md
│   ├── dimensionality-index-cost.md
│   └── ...
├── digests/
│   ├── d01-mteb-multilingual-subsets.md
│   ├── d02-hnsw-memory-scaling.md
│   └── ...
├── memlog                          # append-only; claim tally lives here
└── research.md                     # the canonical cited report

research.md is the artifact. Everything else is the audit trail — and the reason a run that dies at minute 30 resumes instead of restarting.

A.5 Reading the report — where to look first, in order

  1. "Could not determine." Read this before the recommendation. If per-language quality for your two most important languages is in here, the selection matrix is weaker than it looks and you should say so out loud in the ADR.
  2. The selection matrix. Check that the weights are your constraints. If it weighted "ease of getting started" heavily, it defaulted to generic criteria and you under-specified at the plan gate.
  3. Self-reported vs. independent numbers. Every vendor-run benchmark should be flagged. If they aren't, ask.
  4. The staleness map. Turn it into calendar entries the same day. This is the step everyone skips and it's what makes the artifact a living asset rather than a snapshot.
  5. The recommendation, last. By now you know how much weight it can bear.

A.6 When the matrix disagrees with you

This happens, and it's the most useful moment in the process. Two legitimate responses:

  • Your constraint was wrong or unstated. Fix the weights and say why in the ADR. "We overrode the matrix because index memory is a harder ceiling than the weighting reflected" is a fine sentence.
  • Your prior was wrong. Rarer, more valuable.

What's not legitimate is re-running with a different framing until you get the answer you wanted. If you're tempted, that's the moment to turn red_team on instead.

A.7 Handoff

research.md carries metadata frontmatter that downstream skills consume directly:

/bmad-prd

The PRD reads the summary without reprocessing the digests. In your repo, put the report next to the ADR it grounds:

docs/decisions/0014-embedding-model.md          # the decision
docs/decisions/0014-research.md                 # the evidence

Realistic cost: ~40 minutes wall clock, most of it unattended. Compare against the multi-week cost of re-embedding after a wrong pick.


Walkthrough B — Draft → Process: the MT vendor landscape

Scenario. You need translation for the multilingual feature. Six plausible vendors, opaque and structurally different pricing, quality claims that are all self-reported.

This is the textbook Draft case: a broad public sweep over commercial sources, which is exactly what hosted deep-research products are good at, and which you're probably already paying for.

B.1 Draft

> draft a deep research prompt for Gemini: choosing a machine translation
  vendor for [your language pairs], ~[N] million characters/month, EU +
  Canada data residency requirements, glossary/do-not-translate support
  required. Competitive type, select shape.

You get back a prompt carrying the competitive pack's craft — pruned dimensions, freshness bars, source policy, and a hard citation demand, tuned to the tool you named.

Check it before you paste. Two things:

  • Does it demand citations with publication dates? If the citation demand is soft, the report will be soft.
  • Does it name volume and language pairs? Pricing rankings invert with volume, and "supports 100+ languages" is a marketing claim that says nothing about your specific pairs. If those aren't in the prompt, add them by hand.

B.2 Round-trip, then Process

Paste into your subscription tool, let it crawl, save the output.

> there's a research report at ~/Downloads/mt-vendors.pdf, process it

What Process does, and why each part matters:

StepWhy
Original preserved untouched in imports/Provenance. You can always go back to what the tool actually said
Claims extracted to digests/ behind the firewallYour project context can't leak into the extraction
Gap-check against the competitive packThe pack knows what a vendor teardown should cover. This is the highest-value step — it tells you what your external tool missed
Distilled into the standard cited research.md with metadataDownstream skills consume it identically to a native run

B.3 The gap report is the point

Typical output from the gap check on a vendor sweep:

COVERED
  pricing, language coverage, published quality claims, API surface

GAPS (not covered by the imported material)
  - sub-processor lists and data residency commitments
  - training-on-customer-data defaults and opt-out terms
  - glossary / do-not-translate feature parity across vendors
  - exit terms and data portability
  - per-pair quality for [your two lowest-resource pairs]

Note what the gaps are: every single one is a procurement or per-pair specific question. Hosted research tools crawl broad commercial surfaces well and are poor at the narrow, contractual, and specific. That's the division of labour to internalize.

B.4 Targeted Run on the gaps only

> run a focused research pass on the gaps only — sub-processors,
  training-on-customer-data terms, glossary parity, exit terms.
  Type competitive, effort standard, validation high.

Small, cheap, and it lands in the same run folder — the slug is deterministic across the draft→process→refresh lifecycle, so the report is one artifact, not three.

Net cost: one paste, one file drop, one 12-minute run. Your subscription paid for the expensive crawling.


Walkthrough C — The compliance gate, done without fooling yourself

Scenario. Design freeze is in two weeks. You need to know whether the deletion path survives a right-to-erasure request against a vector index.

C.1 The framing trap, shown concretely

❌ Here's our retention and deletion design [paste]. Is it GDPR compliant?

This defeats the firewall by hand. You've supplied the conclusion as context, and you'll get a well-cited report explaining why your design is fine. It will be persuasive. It will be worthless.

✅ What does GDPR Art. 17 require when personal data has been embedded into
   a vector index that does not support hard delete? Include regulator
   guidance on derived data and on bounded-but-delayed deletion.
   Type: domain. Validation: high. red_team on.

Then compare your design against the requirements yourself, afterward. That ordering is the entire discipline.

C.2 Source policy override — do this before running

Regulatory research is the clearest case for overriding the default source policy, because the SEO layer in this space is thick and sales-motivated:

# _bmad/custom/bmad-deep-recon.user.toml

[[workflow.source_policies]]
code = "regulatory"
preferred = [
  "eur-lex.europa.eu", "edpb.europa.eu", "ico.org.uk",
  "priv.gc.ca", "cai.gouv.qc.ca",
]
banned_kinds = ["law-firm marketing", "compliance-vendor content marketing"]

[workflow.freshness.regulatory]
statute     = "36mo"
guidance    = "12mo"
enforcement = "6mo"

Confirm the exact key paths against the shipped customize.toml; the shape and the intent are what matter.

C.3 Two runs, not one

Run 1 (domain) — what the law requires. Erasure scope for derived data, whether embeddings are personal data, Art. 22 threshold for your ranking surface, Quebec Law 25 automated-decision notice, transfer posture for inference calls.

Run 2 (technical) — how to satisfy it in your stack. Real deletion latency under HNSW soft-delete plus segment merge; whether bounded-but-delayed deletion is defensible; consent propagation to precomputed candidate sets and warm caches; embedding inversion as a residual risk.

Merging these produces a report that's shallow on both, because they want different sources and different freshness windows.

C.4 What you hand counsel

Not the whole report. A one-pager built from it:

  • Requirements, each traced to a primary source with a date
  • Our design's position on each — your comparison, done after the fact
  • "Could not determine", verbatim. In compliance work the gaps are the finding
  • Three specific questions you need a lawyer to answer

An hour of counsel time instead of a week. And when the auditor asks "how did you conclude this?", the answer is a file with publishers and access dates, not a recollection.


Walkthrough D — Threat-modeling a ranking surface

Scenario. The recommendation feature ships to authenticated users next quarter. Security review is a launch gate.

D.1 The run

> technical research: attack surface of a personalized recommendation
  ranking system serving authenticated users. Cover manipulation of
  rankings, poisoning of the interaction feedback loop, inference attacks
  against user embeddings, cross-user leakage through shared segments,
  and model extraction. Include documented incidents, not just papers.
  Validation: high. red_team on.

red_team on is deliberate. You are walking in believing the system is basically safe — buy the adversarial pass.

"Include documented incidents, not just papers" is the other lever. Academic attack papers describe what's possible; incident writeups describe what's happened, which is a much better guide to what to fix first.

D.2 The transformation that makes it useful

A threat list is not a deliverable. Convert it, in the same session:

Threat (from report)Likelihood hereImpactDispositionArtifact
Shilling / coordinated profile injectionMedium — accounts are cheapHigh — ranking integrityMitigateStory: interaction-rate anomaly detection per item
Embedding inversion recovering attributesLow — vectors not exposed at APIHigh if exposedControlADR: user vectors never leave the service boundary
Cross-user leakage via shared segmentsMediumMediumMitigateStory: minimum cohort size before a segment is surfaced
Model extraction via systematic queryingLow — rate limits existMediumAccept, documentedNote in threat register, with citation
Feedback-loop poisoning (slow-drip)MediumHigh — hard to detectInvestigateSpike: can our anomaly detection see sub-threshold drift?

Accepted risks get written down with the citation attached. That record is what makes the next security review fast, and it's exactly what's missing when a reviewer asks "did you consider X?" and the honest answer is "probably, in a meeting."

D.3 If there's an LLM in the pipeline

Add indirect prompt injection via item metadata and user-generated content as a first-class dimension, and take the structural lesson from PR #2611 itself: they didn't filter untrusted text out of a shell command, they changed the channel so it never reached one. Briefs went to a file; the template substitutes a skill-generated path. Filters on untrusted text fail eventually. Removing the interpreter from the path doesn't.


Walkthrough E — Refresh, three months later

> refresh the embedding model research

Only stale claims get re-verified, per the staleness map. You get a delta:

CONFIRMED (11)   license terms, dimensionality/memory figures, ...

CHANGED (3)
  [4] Model B latency benchmark — improved ~20% after a runtime release
  [7] Model C pricing — restructured to committed-use tiers
  [9] Model A release cadence — slowed; last release 5 months ago

OVERTURNED (1)
  [12] Model D commercial-use terms changed; the revenue threshold that
       made it viable for us no longer applies as documented

⚠  Claim [12] is referenced by:
       docs/decisions/0014-embedding-model.md

That warning is the whole reason the lifecycle exists. A licensing assumption underpinning a shipped decision moved, and you found out from a 6-minute refresh rather than from procurement, or a lawyer, or a blog post someone forwards you.

What you do: re-open the ADR, note the change, decide whether it changes anything. Often it doesn't. The value is that the question got asked at all.


Walkthrough F — Deepen one dimension

You committed to a model. Now you need the fine-tuning story properly, and only that.

> deepen the fine-tuning and adaptation dimension

Drills one dimension without re-running the other five. Minutes, not tens of minutes. This is the command that makes research iterative — you stop trying to front-load every question into one enormous engagement and instead pull threads as they become load-bearing.

Rule of thumb: deepen when the question was in scope but under-answered. A fresh run when the question is genuinely new.


Walkthrough G — Headless refresh in CI

The headless JSON contract means the staleness map can run itself. A quarterly job, roughly:

# .github/workflows/research-refresh.yml
on:
  schedule: [{ cron: "0 6 1 */3 *" }]      # first of the month, quarterly
jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Refresh decision research
        run: |
          bmad-deep-recon refresh \
            --headless --output-format md \
            --workspace docs/decisions/research/
      - name: Open an issue if anything was overturned
        run: ./scripts/alert-on-overturned.sh

Headless runs present canonical markdown only — output_format = auto handles that without configuration. Alert on overturned and on changed that touches a referenced claim; ignore confirmed. Otherwise you've built a quarterly notification everyone mutes by the second quarter.

This is the cheapest high-value automation in the whole system: decision rot, detected without anyone remembering to look.


Walkthrough H — The anti-walkthrough

Worth seeing the failure mode concretely, because it's what a first run usually looks like.

> research recommendation system best practices

What the plan gate proposes: eleven dimensions, breadth-first, deep, ~55 minutes. Nothing was pruned, because no decision was stated and therefore nothing could be pruned.

What you get: a well-cited, genuinely accurate survey of recommender systems. Collaborative filtering, content-based, hybrids, cold start, evaluation, MLOps. All true. All findable in any textbook. None of it weighted to a choice you face.

Cost: ~55 minutes and a large token spend, for something you skim once.

What went wrong, precisely: the plan gate is a pruning device. It prunes dimensions against the decision. No decision means no pruning function, so the run is unbounded by construction. The mechanism didn't fail — it was handed nothing to work with.

The fix is one sentence, always the same shape: "I am choosing between X, Y and Z, under constraints A, B and C, and I'll live with it for N months."

If you cannot write that sentence, you're not ready to research. You're ready to brainstorm — which is a different skill, and the right one for that moment.


23.9 Team integration patterns

Keep the research in the repo, next to the decision.

docs/decisions/
├── 0014-embedding-model.md          # ADR: what we chose and why
├── 0014-research.md                 # the evidence, with provenance
├── 0015-mt-vendor.md
└── 0015-research.md

The ADR states the decision and the reasoning. The research file carries the citations, dates, and staleness map. Six months later the ADR tells you what, and the research file tells you whether it still holds.

Review research in PRs. A research report in a pull request gets the same treatment as code: someone checks whether the plan-gate dimensions were right, whether the weights match the team's actual constraints, and whether the "could not determine" section was read rather than skipped. This catches motivated framing better than anything else, because the person reviewing didn't want the answer.

Onboarding. A new engineer reading 0014-research.md learns why the stack looks the way it does, from sources, in twenty minutes. Compare with the usual mechanism: asking whoever's been there longest and getting a partly-reconstructed memory.

Don't let it become theatre. Research that's produced, filed, and never refreshed or contradicted is a compliance ritual. The signal that it's real: someone, at some point, overrode a recommendation and wrote down why.

23.10 Calibration — what to spend where

EngagementPresetValidationRed teamTimeWorth it when
Library or framework pickquicknormaloff~8 minMulti-year dependency
Dependency health checkquicknormaloff~6 minAlways. Cheapest useful run there is
Metric / eval methodologystandardnormaloff~20 minThe metric will govern future decisions
Component selection (model, index)standarddeephighoff25–45 minReversal cost is weeks
Vendor / build-vs-buydeephighoff35–50 minData leaves your boundary
Privacy / regulatorydeephighon40–60 minAny personal data or profiling
Security / threat modeldeephighon40–60 minUser-facing or model-driven surface
Algorithm / technique trendquickstandardnormaloff10–20 minOnly after converting it to a decision

Two patterns in that table worth naming:

  • red_team is on exactly where you have a prior. Nobody walks into a privacy or security review neutral — you walk in believing you're fine. That's the condition the adversarial pass exists for.
  • The cheapest runs are the most consistently underused. A six-minute dependency-health check has probably prevented more grief than any model comparison ever run.

24. Sources

  • BMAD-METHOD PR #2611 — "feat(core): consolidate research trio into bmad-deep-recon", merged 23 Jul 2026 — https://github.com/bmad-code-org/BMAD-METHOD/pull/2611
  • docs/explanation/deep-recon.md — https://github.com/bmad-code-org/BMAD-METHOD/blob/main/docs/explanation/deep-recon.md
  • docs/how-to/customize-bmad.md (config layering and merge rules) — https://github.com/bmad-code-org/BMAD-METHOD/blob/main/docs/how-to/customize-bmad.md

Applied guidance in §12–§17, the epistemological analysis in §22, and the walkthroughs in §23 are my analysis, not sourced from the BMAD docs. The named philosophical concepts (Gettier, Popper, Reichenbach, Condorcet) are standard terms of art in epistemology and philosophy of science.

Source: bmad-rcon-foundation · bmad-rcon-foundation.md · updated 2026-07-25 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

Part 0 — Foundations

Read this before bmad-deep-recon-guide.md. It assumes no background in agentic tooling. Every term is defined, traced to where its name came from, and shown in use.

Why the etymologies? Almost every word in this system is borrowed from somewhere else — the military, graph theory, newspaper typesetting, caching, law. That's not decoration. When you know that "topology" comes from network diagrams and "breadth-first" is a graph-traversal algorithm, you stop memorizing vocabulary and start predicting what the system will do, because you already understand the source concept. Naming is compressed explanation.


Contents


1. The five-minute model, before any vocabulary

Strip away every term and this is the whole system:

You have a decision to make. You don't trust the AI's memory, because it's stale and can't show its work. So you make it go look things up, in parallel, from sources it has to name — while preventing your own assumptions from leaking into what it finds. Everything it learns gets written to a file with dates attached, so that in six months you can tell whether it's still true.

Four moving parts:

  1. A decision — stated up front, with constraints. Everything else is shaped by it.
  2. A plan you approve — what will be investigated, how, and for how long.
  3. Parallel lookup with a barrier — several sub-processes search, none of them sees your assumptions.
  4. A dated, cited file — the durable output, with an expiry warning built in.

If you hold only that, the rest of the vocabulary is just precise names for these four things.


2. Layer 1 — the AI substrate

You need these before BMAD makes sense. Skip any you already know.

2.1 The basics

TermWhat it isWhere the name comes fromHow it shows up here
Model (LLM)The trained neural network that produces text. "Large Language Model"A model in the scientific sense: a compressed representation of something, used to make predictionsClaude, GPT, Gemini. Different subagents can run different models
TokenThe unit of text a model reads and writes — roughly ¾ of a word, or a word fragmentThe philosopher C. S. Peirce's type/token distinction: a "type" is the abstract pattern, a "token" is one concrete instance of it. NLP borrowed it for "one unit of text"Every instruction file costs tokens. This is why BMAD obsesses over file size
Context windowThe total amount of text the model can hold at once — the instructions, your messages, the tool results, everything. FiniteWindow from signal processing: a bounded view sliding over a longer stream. You see a portion, not the wholeThe hard constraint behind almost every BMAD design choice. Run out and the model starts forgetting the beginning
PromptThe text you send inTheatre — the prompter feeds an actor their line from offstageDraft mode's entire output is a prompt
InferenceOne act of the model producing outputLogic/statistics: deriving a conclusion from inputs. Contrasted with training"Inference cost" = what you pay per call
Training data / knowledge cutoffWhat the model learned from, and the date it stopsSelf-explanatoryThe reason "no conclusions from training data" exists as a rule
HallucinationThe model stating something false with full confidenceBorrowed from psychiatry — perceiving what isn't there. A contested term: critics argue it implies a perceptual glitch when the real cause is that the model was never tracking truth in the first place. "Confabulation" is the more accurate wordThe failure mode the citation discipline exists to prevent
GroundingTying output to a verifiable external sourceElectrical grounding — connecting a circuit to earth so it has a stable referenceThe entire point of Deep Recon

2.2 Agents and tools

TermWhat it isWhere the name comes fromHow it shows up here
AgentA model that can take actions in a loop — call tools, observe results, decide what to do next — rather than just answering onceLatin agere, "to do." In AI theory, a rational agent perceives an environment and acts on it (the framing in Russell & Norvig's standard textbook)⚠️ BMAD uses this word differently — see §3
Tool / function callingGiving the model a set of callable functions (search the web, read a file, run a command)LiteralSearch, file writes, and MCP servers are all tools
HarnessThe program that runs the loop around the model, handles tool calls, and manages filesFrom draft animals and, later, test harnesses in software: the rigging that connects a component to the thing driving itClaude Code, Cursor, etc. BMAD skills run inside a harness
SubagentA separate model instance spawned with its own fresh context and a narrow assignment, which reports backSub- = below. It's an agent working for an agentThe parallel researchers. Also called assistants in the docs
MCP (Model Context Protocol)An open standard for connecting models to external data sources and toolsLiteral descriptionHow Deep Recon reaches Tavily, Perplexity, or your internal systems

Why subagents exist at all — this is worth understanding, because it explains the whole architecture. The context window is finite. If one model does twelve searches, it accumulates twelve pages of raw text and chokes. Instead: spawn six subagents, each with its own fresh window, each doing two searches and returning a half-page summary. The parent receives six half-pages instead of twelve full pages.

The cost: each subagent only knows what you told it. Coordination becomes the hard problem. Nearly every complexity in Deep Recon — briefs, digests, the single-writer rule — is machinery for managing that trade.


3. Layer 2 — how BMAD is built

3.1 The naming collision, flagged early

WordAI meaningBMAD meaning
AgentA model acting in a loop with toolsA persona — "Mary the Analyst" — that presents a menu and routes you to workflows
Skill(no standard AI meaning)A folder of markdown instructions the model loads on demand

When BMAD docs say "the Analyst agent," they mean a persona file, not an autonomous process. Both senses appear in the same repo. Read from context.

3.2 Structural vocabulary

TermWhat it isWhere the name comes fromHow you use it
SkillA directory with SKILL.md plus supporting files. The unit of installation and invocationA packaged competence the agent acquires — the way a person learns a skill and can then perform it on demandYou invoke it: /bmad-deep-recon
SKILL.mdThe entry file. A router: reads your request, decides which branch, loads the right procedureConventionYou never edit it directly; you override via TOML
references/Procedure files loaded only if that branch is takenLiteralWhy the skill shrank from ~4,000 to ~2,100 tokens: detail moved here
Just-in-time loadingLoading instructions only when neededBorrowed from just-in-time manufacturing (Toyota, 1970s): don't stock parts until the moment of use, because inventory is wasteThe core token-optimization pattern. Context is the inventory
Slash commandTyping /name to invoke somethingIRC and chat clients, 1990s: /join, /meHow you start every skill
WorkflowA multi-step skill with a defined sequenceBusiness process managementDeep Recon is a workflow
ModuleA bundle of skills shipped together. core = everything; bmm = software developmentSoftware modularityDeep Recon moved from bmm to core in PR #2611, so non-software installs get it
ShimA stub that forwards an old name to a new oneMechanical: a thin wedge inserted to fill a gap or level a surface. Software borrowed it for thin compatibility layersbmad-market-research still works; it forwards to Deep Recon with type=market
TOMLThe config file format (customize.toml)"Tom's Obvious Minimal Language," named for its creator, Tom Preston-WernerWhere you set validation level, source policies, output format
FrontmatterA metadata block at the top of a markdown fileBook publishing: the front matter is everything before the main text — title page, copyright, contents. Static site generators (Jekyll, ~2008) borrowed it for the YAML blockCarries research.md's provenance so downstream skills can consume it
ArtifactA file produced by a process, kept as evidence of itArchaeology: an object made by a human, evidence of activity. Software took it for build outputsresearch.md is the artifact; the memlog is its excavation record
Planning artifactsThe output folder where analysis and planning documents landCompound of the aboveWhere your run folders go
MemlogAn append-only file recording what happened, in orderMemory + log. Append-only is the load-bearing part: you add, never editSurvives context death. The claim tally lives here

Why "append-only" matters — if you can rewrite history, the record can't be trusted, because you can't distinguish "this was always true" from "someone changed it." Append-only logs are the backbone of databases, version control, and blockchains for exactly this reason. Here it means: a claim's status can be added to (unverified → verified → overturned), and the earlier states remain visible.


4. Layer 3 — Deep Recon's own vocabulary

4.1 "Recon"

Military reconnaissance: scouting ahead of the main force to learn the terrain and the enemy's position before committing troops. From French reconnaître, "to recognize" — literally to know a place again, by having been there.

Why it's the right metaphor:

  • It precedes commitment. You scout before you move, not during.
  • It's cheap relative to what it protects. A patrol costs far less than a failed assault.
  • It's information-gathering, not action. Recon doesn't take the hill. It tells you whether the hill is takeable.
  • "Deep" recon is a real military term: patrolling far beyond the forward line, at higher risk, for strategic rather than tactical information.

Where the metaphor is useful to you: it correctly implies that research is preparatory and bounded. Research that never terminates in a decision has failed at being recon.

4.2 "Run" — the term you asked about

What it is: one complete, bounded execution of a research engagement. It has a beginning, an end, a configuration, an identifier, a folder on disk, and outputs. When you say "the run," you mean that whole bounded episode and everything it produced.

Where the name comes from — three overlapping ancestors:

  1. Manufacturing. A production run: one batch produced under one set of settings. Change the settings, it's a different run. This gives you the sense of bounded, configured, and reproducible-ish.
  2. Early computing. In batch-processing days, you submitted a job and the operator ran it. One run = one pass of one job through the machine, producing one printout. This gives you one execution, one output.
  3. Machine learning experiment tracking — almost certainly the immediate ancestor. In tools like MLflow and Weights & Biases, a run is one execution of an experiment: it gets a run ID, a config, logged metrics, and an artifact directory. That's exactly Deep Recon's structure — a slug, a config, a memlog, an artifact folder.

Why the word does real work here. Compare the alternatives:

  • "Session" would imply it's tied to your conversation. It isn't — it survives on disk.
  • "Query" would imply one question, one answer. It's dozens.
  • "Report" names only the output, not the process.
  • "Run" names the bounded episode plus its record. That's the concept you need, because you refresh a run, deepen a run, resume a run.

How you use it — the three senses, which trip up beginners:

SenseExampleMeaning
The mode"use Run mode, not Draft"Do the research natively in this session, rather than drafting a prompt for another tool
The episode"the run took 40 minutes"This particular bounded execution
The verb"run the research"Execute it

Practical consequences of a run being a bounded, identified thing:

  • It has an identity. The folder slug is deterministic, so a draft, its later processing, and a refresh three months on all land in the same run folder. One decision, one run, one artifact — not three loose files.
  • It's resumable. Because everything hits disk as it arrives, a run that dies at minute 30 picks up from disk rather than starting over.
  • It's refreshable. refresh re-executes only the stale parts of an existing run. You can only do that to something with an identity.
  • It's configurable as a unit. Effort, validation, and freshness are properties of the run, set once at the plan gate.

4.3 "Mode" — Draft, Process, Run

Mode = a way of operating; from Latin modus, "measure, manner." Same sense as a camera's portrait/night mode: the same device, different operating logic.

The three are named for what you do, and they differ in where the searching happens:

ModeWhere the crawling happensYou supplyPick it when
DraftElsewhere — ChatGPT, Gemini, PerplexityOne paste, one round-tripYou already pay for a deep-research subscription
ProcessAlready happened, somewhereA finished reportSomeone handed you a document
RunHere, in this sessionApproval at one gateYou want it now, or you need tools only this session has

4.4 "Type" and "pack"

Type = the category of research: market, domain, technical, competitive, user-voice, academic-lit.

Pack = the ~25-line configuration card that a type selects. From the gaming/software sense of a pack: a bundle of assets shipped as a unit (texture pack, expansion pack).

A pack contains: prioritized dimensions, source craft, freshness rules per claim class. This is the "data-driven variation" that let three near-identical workflows collapse into one skill — the procedure is shared, only the card differs.

Decision shape is a second, independent axis: explore (build understanding) or select (choose between options, using a weighted matrix). Shape because it describes the form the conclusion takes. Any type can combine with either.

4.5 "Dimension" — the second term you asked about

What it is: one independent axis of inquiry within your research question. Your question gets decomposed into dimensions; each one is investigated separately, often in parallel; the results are recombined.

Where the name comes from: geometry and measurement. A dimension is an independent direction of variation — you can move along one without moving along the others. Length, width, height. Data analysis borrowed it (a "dimension" in a data cube: time, region, product), and the sense carried is the same: a separable axis.

Why the word is well chosen — it encodes three properties at once:

  1. Independence. Dimensions should be as orthogonal as possible, because that's what makes parallel investigation valid. If two dimensions overlap heavily, you're paying twice for one answer.
  2. Coverage. Together they should span the decision. In geometry, dimensions that don't span the space leave regions you can't reach; a dimension set that doesn't span the decision leaves blind spots. That's what the "could not determine" section reports.
  3. Separability. Each can be handed to a different subagent with a self-contained brief.

How you use it — this is the highest-leverage skill in the whole system. At the plan gate you're shown a proposed dimension list. Your job:

ActionWhenWhy it matters
Delete a dimensionIt doesn't bear on the decisionEach costs a full fan-out. At deep, cutting one saves ~15% of the run
Add a dimensionSomething decision-critical is missingThe pack is generic; only you know your constraints
Split a dimensionIt's actually two questions with different sourcesMerged dimensions produce shallow answers on both
Merge twoThey'd hit the same sourcesRemoves duplicated effort

Concrete example from the guide: for an embedding-model choice, the proposed dimensions covered benchmarks, cost, license and maintenance — but omitted behavior under filtered ANN search. Benchmarks measure unfiltered top-k retrieval; your recommender queries with filters. That missing dimension is the difference between a report that answers your question and one that answers a similar-looking question.

4.6 "Topology" — and where breadth-first/depth-first come from

What it is: the shape of how subagents are arranged against the dimensions — how work is split and connected.

Where the name comes from: mathematics. Topology studies properties of shape that survive stretching and bending — what's connected to what, regardless of distance. Network topology (star, ring, bus, mesh) is the borrowing you've probably met: the diagram of what connects to what, ignoring cable length.

Here it means the same: the connection pattern of the work graph. Not how long each part takes — the shape.

The three topologies:

TopologyShapeUse when
Breadth-firstOne plan → N assistants → N independent sub-questions, in parallelThe dimensions are genuinely independent (the usual case)
Depth-firstOne question, several assistants attacking it from different angles, iteratingOne contested question where the disagreement is the problem
StraightforwardOne assistant, small budgetA lookup. Ten agents on an easy question just burns tokens

Where "breadth-first" and "depth-first" actually come from — and this is the good part. They are graph-traversal algorithms from computer science.

Imagine exploring a maze:

  • Depth-first search (DFS): pick a corridor, follow it as far as it goes, hit a dead end, back up, try the next. You go deep before you go wide. Traces back to Trémaux's 19th-century maze algorithm; formalized in mid-20th-century CS.
  • Breadth-first search (BFS): step one pace down every corridor, then two paces down every corridor, and so on. You go wide before you go deep. Developed in the 1950s (Moore, for maze routing; Lee, for circuit routing).

The classic property: BFS finds the shortest path and explores evenly; DFS goes further faster on one path but can get lost down a long wrong corridor.

The metaphor transfers exactly:

  • Breadth-first research: cover all six dimensions to a moderate depth, evenly.
  • Depth-first research: drive hard down one question, following where it leads, accepting you'll learn less about everything else.

Knowing the algorithms means you can predict the failure modes. BFS's weakness is memory — it holds the whole frontier at once, which is exactly why breadth-first research burns more tokens per round. DFS's weakness is going deep down a corridor that doesn't matter — which is exactly what happens when you depth-first a question that turns out to be a side issue.

Fan-out — a related term. From digital electronics: the number of gate inputs a single output can drive before the signal degrades. Distributed systems borrowed it for one request spawning many downstream calls. Here: one plan spawning N assistants. The electronics sense carries a useful warning — fan-out has a limit past which quality degrades. More parallelism is not free, and it is not automatically better.

4.7 "Plan gate"

What it is: the single mandatory stop where the system shows you what it intends to do, and waits.

Where the name comes from: gate in the sense of a controlled passage — nothing proceeds until it opens. Two direct ancestors:

  1. Stage-Gate in product development (formalized by Robert G. Cooper in the 1980s): projects move through stages, and between them sit gates where a decision-maker says go, kill, hold, or recycle. The point of a gate is that it's the only place a decision is made, so the decision gets real attention.
  2. Quality gates in CI/CD: the build doesn't advance unless it passes.

Why there is exactly one. Constant interrogation trains you to click through. One well-placed gate that actually matters gets read. After it, the run proceeds with light checkpoints. This is a deliberate attention-budget decision, and it means your attention at the gate is the single highest-value input you provide to the entire system.

What it shows: the decision, the dimensions, the topology, the knobs, and a time estimate. What you do: prune, add, adjust, check that the estimate matches your actual patience.

4.8 Rounds, leads, budgets, presets

TermWhat it isWhere the name comes from
RoundOne full cycle of parallel searching, after which results are assessed and the next cycle assignedBoxing, tournaments, negotiations: a bounded phase followed by reassessment
LeadA finding that points toward something worth chasing next — a contradiction, an unexpected connectionJournalism: a lead is a tip pointing at a story. Round 2's assignments come from round 1's leads
Tool-call budgetA cap on how many searches one assistant may makeOld French bougette, "little pouch" — a bounded allocation. Prevents one assistant eating the whole run
Effort presetA named bundle of settings: quick / standard / deepPreset from audio and camera equipment: stored settings recalled by name
Stop-and-write valveThe rule that says commit what you have rather than spirallingValve: a pressure-release mechanism

Precedence — worth memorizing: your request beats a pinned setting beats the preset. Whatever you say in the moment wins.

4.9 Briefs, digests, claims

TermWhat it isWhere the name comes from
BriefThe narrow assignment handed to one subagent — written to a file, never a command lineLegal brief (a condensed statement of a case) and military briefing. Both mean: the minimum someone needs to act, deliberately short
DigestExtracted, arranged claims from a sourceLatin digerere, "to carry apart, arrange." Justinian's Digest (6th c.) organized scattered Roman law into a usable body. A digest is not a summary — it's rearranged for use
ClaimA single assertion, carrying a source, dates, and a statusLegal and philosophical: an assertion put forward as true, awaiting support
StatusunverifiedverifieddisputedoverturnedLatin status, "state." The point is that a claim's condition is tracked and visible, not silently promoted to fact
ImportA finished external report, preserved untouchedTrade: something brought in from outside, whose origin is recorded

Why briefs are written to files and not passed as text on a command line — the security fix in PR #2611, and worth understanding as a beginner because it generalizes. If untrusted text is pasted into a shell command, characters like $(...) or backticks are interpreted as commands rather than read as text. Researched content is untrusted by definition — you fetched it from the internet. The fix wasn't to filter dangerous characters (filters always leak); it was to change the channel so the text never reaches an interpreter. It goes to a file; the command references the file's path.

That pattern — remove the interpreter from the path rather than sanitizing the input — is one of the most transferable ideas in the whole PR.

4.10 "Firewall"

What it is: the rule that your project context may shape which questions get asked, but never what counts as evidence. Subagents receive only their brief.

Where the name comes from: building construction. A firewall is a physical wall built to a fire-resistance standard, dividing a structure into compartments so a fire in one can't spread to the next. Cars borrowed it (the barrier between engine and cabin), then networking borrowed it in the late 1980s.

The property that carries through every sense: a barrier at a boundary that stops one specific thing from propagating, by design and by construction rather than by vigilance.

Why it matters more than it sounds. The failure it prevents is subtle: hand an AI your architecture document and ask it to research alternatives, and it comes back explaining that your architecture is excellent. Not through deceit — through ordinary confirmation bias. The thing generating hypotheses is also grading them.

Most attempts to fix this are instructions — "be objective," "consider alternatives." Instructions fail, because a system inclined to agree with you will find a way. The firewall is architectural: the subagent cannot be biased by context it was never given. That's the difference between a policy and a wall.

The closest analogy from research methodology is the double-blind trial: the experimenter doesn't know which subject got the drug, so their expectations cannot contaminate the measurement. Not "must not" — cannot. Same idea, same reason it works.

4.11 Staleness, freshness, refresh, deepen

TermWhat it isWhere the name comes from
StalenessA claim that was true when gathered and may not be nowLiteral — stale bread. Borrowed into caching, where data past its TTL is "stale": still present, still readable, no longer trustworthy. That's precisely the condition
Freshness windowHow long a class of claim stays trustworthy. Statute ~36 months; enforcement guidance ~6Same caching lineage — essentially a TTL per claim class
Staleness mapThe section of the report listing what ages fastest and when to re-checkCartographic metaphor: a map of where the ground is likely to move
RefreshRe-verify only the stale claims; produce a delta reportCaching again: refreshing a cache entry re-fetches it
DeltaThe difference: confirmed / changed / overturnedGreek letter Δ, standard notation for change
DeepenDrill further into one dimension without re-running the othersLiteral

The distinction that matters: refresh when time has passed. deepen when a dimension was under-answered. A brand-new question is neither — that's a fresh run.

4.12 Provenance, citations, verification

TermWhat it isWhere the name comes from
ProvenanceThe documented origin and chain of custody of a claimFrench provenir, "to come from." Art history and archaeology: the ownership record that establishes an artifact is what it claims to be. A painting without provenance may still be genuine — but you can't show that it is
Citation markerThe inline [n] pointing to a sourceLatin citare, "to summon" — literally calling a witness
Source appendixThe numbered list the markers resolve toLatin appendere, "to hang upon" — attached matter
Cross-checkThe mechanical script verifying every marker has an entry and vice versaBookkeeping: reconciling two records against each other
Verification levelnormal / high / max — how much gets checkedLatin verus, "true" — making true, or showing to be so
Red teamAn adversarial pass that attacks the conclusionCold War military war-gaming: Red played Soviet forces, Blue played US. The Red team's job was to win against the plan. Security adopted it in the 1990s

Why provenance is the right word, not "sources." A source list says where things came from. Provenance adds when it was published, when you accessed it, and who published it — which is what lets someone else evaluate a claim they didn't verify themselves. That's the difference between a bibliography and an audit trail.

4.13 The remaining machinery

TermWhat it isWhere the name comes from
SlugThe short deterministic identifier for a run folderNewspaper typesetting. A "slug" was a line of metal type cast by a Linotype machine; the slugline was a short label identifying a story as it moved through production. Web development borrowed it for URL identifiers
DeterministicSame input always produces the same outputLatin determinare, "to limit." Why it matters: the run slug must be identical across draft → process → refresh, or your artifacts scatter
Single-writer ruleSubagents return digests to the lead; only the lead writes research.mdConcurrency control. Parallel writes to one file race and corrupt. Fan out reads, serialize writes
HeadlessRunning without an interactive user or displayHeadless server / headless browser: no "head" = no monitor. Here it means CI, so output defaults to plain markdown
IdempotentRunning it twice has the same effect as running it onceLatin idem ("same") + potens ("power"). Why refresh is safe to schedule
Determinism kitrecon_kit.py — the script owning counting, date math, and slug generationBecause models are unreliable at mechanical tasks and excellent at judgment. Give each what it's good at

5. Six mental models that carry the most weight

If you remember nothing else:

1. Recon is scouting before you commit troops. Cheap relative to what it protects, and it must terminate in a decision or it wasn't recon.

2. The run folder is a lab notebook. Dated, append-only, showing failed attempts alongside successes. The value isn't the conclusion; it's that someone else can check your work — including future you.

3. The firewall is a double-blind trial. The researcher can't be influenced by expectations they were never given. Architectural, not aspirational.

4. Dimensions are axes that must span the space. Independent enough to parallelize, complete enough to cover the decision. Gaps in the span are exactly what "could not determine" reports.

5. Staleness is cache TTL for beliefs. Still readable, no longer trustworthy. Different claim classes have different TTLs, and that's a feature.

6. The plan gate is a stage gate. There is exactly one, deliberately, so that it gets read. Your attention there is the highest-value input you give the system.


6. Where the metaphors break

Understanding a borrowed term includes knowing where the borrowing stops. Four that mislead beginners:

MetaphorWhat it correctly suggestsWhere it misleads
"Run"Bounded, configured, identified, resumableImplies determinism. Two runs with identical settings will not produce identical reports — the model is stochastic and the web changes underneath you. It's a run in the experiment-tracking sense, not the build sense
"Dimension"Separable axes of inquiryImplies true orthogonality. Real research dimensions overlap constantly — license terms bear on maintenance risk, latency bears on cost. Treat them as mostly separable and expect some duplicated effort
"Firewall"A barrier that blocks propagation by constructionImplies a static perimeter. It's a data-flow rule about what enters a subagent's context — and you can defeat it by hand, simply by pasting your assumptions into the request. It protects against accidental leakage, not against you
"Verification"Checking whether a claim holdsImplies independent confirmation. The system checking a claim is the same system that produced it — that's a coherence check, not corroboration. Genuinely weaker than the word suggests

That last one is the most important caveat in this entire document. Deep Recon does not give you knowledge. It gives you auditable justification — a chain you can inspect and challenge. That's a real improvement over uncited fluency, and it is less than a well-formatted, thoroughly-cited report can look like.


7. Learning path: your first two weeks

Week 1 — mechanics

Day 1 — Install and read the shape.

npx bmad-method install

Then /bmad-customize bmad-deep-recon and just read the options. Don't change anything. You're building a map of what's adjustable.

Day 2 — The calibration run. This is the single most useful beginner exercise, and almost nobody does it:

Run a quick engagement on a question you already know the answer to cold — something in your own stack you could write a memo about from memory.

Because you know the content, you're free to evaluate the machinery. Where did it go for sources? Did it find the thing you know is the definitive reference? Where was it confidently thin? Where did it flag uncertainty you'd have glossed over? Twenty minutes here teaches more about calibration than five real runs.

Day 3 — Deliberately break it. Run the anti-pattern: research recommendation system best practices. Watch the plan gate propose eleven dimensions and fifty-five minutes. Kill it at the gate. You'll have learned viscerally why a decision statement is load-bearing, for the price of two minutes.

Day 4 — A real cheap run. Pick a genuine but low-stakes decision — a library choice, a dependency health check. quick, standard validation. Take the output to a colleague and defend it.

Day 5 — Read the artifacts. Open the run folder. Read a brief, a digest, the memlog, then research.md. Trace one claim from the report back through its digest to its source. Once you've done this once, you'll never over-trust a report again.

Week 2 — judgment

Day 6–7 — Draft → Process. Draft a prompt, run it in whatever deep-research tool you have, process it back. The gap report is the lesson: see for yourself what hosted tools systematically miss.

Day 8 — Configure. Write your first user.toml. Set a source policy for one domain you know well — you can tell good sources from bad in your own field, which is exactly what you need to write a policy.

Day 9 — A load-bearing run, with red_team on. Notice how the adversarial pass changes the tone.

Day 10 — Refresh something. Even a two-week-old run. Watch the delta mechanism work.

The habit that makes it stick

Every time you're about to make a decision you'll live with for months, write this sentence first:

"I am choosing between ___, ___ and ___, under constraints ___, ___ and ___, and I'll live with it for ___ months."

If you can write it, you're ready to research. If you can't, you're not ready to research — you're ready to brainstorm, which is a different skill and the right one for that moment.


8. Resources and references

Verify before citing. These are from my own knowledge, not fetched and checked in this session. Titles and authors I'm confident about; URLs and current versions you should confirm.

8.1 BMAD itself — start here

ResourceWhy
github.com/bmad-code-org/BMAD-METHODThe repo. Read docs/explanation/ before docs/how-to/ — explanation gives you the model, how-to gives you the steps
docs/explanation/deep-recon.mdThe primary source for everything in Part 0 and the main guide
PR #2611The rationale, the v1 post-mortem, and the security review. Reading a merged PR discussion is one of the fastest ways to understand why a system is shaped the way it is
docs/how-to/customize-bmad.mdConfig layering and merge rules
The skill's own customize.tomlThe authoritative list of knobs, always ahead of any documentation

8.2 Agentic systems generally

ResourceWhy
Anthropic, "Building Effective Agents" (engineering blog, 2024)The clearest short treatment of when to use workflows vs. agents, and why simple beats clever. Deep Recon's topology choices make more sense after this
Model Context Protocol docs — modelcontextprotocol.ioThe standard behind external_sources
Anthropic's Agent Skills documentationThe skill-as-loadable-competence pattern BMAD builds on
Simon Willison's blog, prompt-injection tagThe best sustained writing on why untrusted text in agent pipelines is dangerous. Directly relevant to the §4.9 file-vs-shell fix

8.3 The computer science behind the terms

TopicWhere to learn it
BFS / DFSCormen, Leiserson, Rivest & Stein, Introduction to Algorithms (CLRS), graph algorithms chapters. Or any decent visualization — 20 minutes watching BFS and DFS explore a maze is worth more than the chapter
Fan-outAny digital logic text for the electronics origin; distributed-systems literature for the modern sense
Append-only logsKleppmann, Designing Data-Intensive Applications, ch. 3 — the best explanation of why immutable logs are the backbone of trustworthy systems
Single-writer / concurrencySame book, ch. 7
Cache TTL and stalenessAny HTTP caching primer; the concept transfers directly

8.4 Epistemology — for §22 of the main guide

All free online:

ResourceWhy
Stanford Encyclopedia of Philosophy (plato.stanford.edu) — entries on Epistemology, The Analysis of Knowledge, Epistemological Problems of Testimony, Scientific ObjectivityRigorous, free, written for non-specialists. The single best starting point
Edmund Gettier, "Is Justified True Belief Knowledge?" (1963)Three pages. Genuinely readable in ten minutes, and it reframes what "justification" has to do
Karl Popper, The Logic of Scientific Discovery (1934/1959)Falsification — the intellectual ancestor of the red-team pass
Hans Reichenbach, Experience and Prediction (1938)Where the discovery/justification distinction is articulated — the firewall's ancestor
Condorcet's jury theorem (SEP or Wikipedia)Why independent errors make crowds reliable — and therefore why correlated subagents don't deliver what fan-out promises

8.5 Research methodology — best practices worth stealing

ResourceWhy
PRISMA guidelines for systematic reviewsThe gold standard for transparent evidence synthesis: pre-registered questions, documented search strategy, explicit inclusion criteria, reported exclusions. Deep Recon is a lightweight cousin; PRISMA shows you what rigorous looks like
GRADE frameworkHow medicine rates the certainty of a body of evidence, not just individual studies. Directly applicable to reading a research report
Literature on publication bias and the replication crisisWhy "surveying published results" systematically overestimates. Justifies the negative-results dimension
Ferrari Dacrema, Cremonesi & Jannach, "Are We Really Making Much Progress?" (RecSys 2019)Specifically for your field: shows that many published recommender improvements fail to beat properly-tuned simple baselines. If you read one paper from this list, read this one

8.6 Domain references for the applied sections

Retrieval and recommendation

  • MTEB (Muennighoff et al.) and BEIR (Thakur et al.) — the benchmark suites, and more importantly their stated limitations
  • Malkov & Yashunin, HNSW (2016/2018) — the original paper for the index you're running
  • Shilling / profile-injection attack literature in the recsys venues (RecSys, SIGIR)

Security and adversarial ML

  • OWASP Top 10 for LLM Applications — the standard reference for prompt injection and agentic risk
  • MITRE ATLAS — adversarial threat landscape for AI systems, structured like ATT&CK
  • NIST AI Risk Management Framework
  • Shokri et al. (2017) on membership inference; Morris et al. on text-embedding inversion

Privacy and regulation — always primary sources, never summaries

  • GDPR text on EUR-Lex; EDPB guidelines and opinions
  • ICO (UK) — unusually clear practical guidance, useful even outside UK scope
  • OPC Canada (priv.gc.ca) and CAI Québec (cai.gouv.qc.ca) for PIPEDA and Law 25
  • The EU AI Act and DSA texts on EUR-Lex

Decision records

  • Michael Nygard, "Documenting Architecture Decisions" (2011) — the origin of the ADR format, and the natural home for research output. Short and practical

9. What I'm confident about, and what I'm not

Worth stating plainly, since this document makes a lot of etymological claims.

Well-documented and standard:

  • BFS/DFS as graph-traversal algorithms; the maze intuition
  • Fan-out from digital electronics
  • Firewall from building construction → automotive → networking
  • Red team from Cold War military war-gaming
  • Slug from newspaper typesetting
  • Frontmatter from book publishing
  • Provenance from art history and archaeology
  • Stage-Gate from Robert G. Cooper's product-development work
  • Shim from mechanical engineering
  • Digest from Latin digerere and Justinian's Digest
  • Reconnaissance from French reconnaître
  • TOML named for Tom Preston-Werner
  • The philosophical works, authors and dates in §8.4

My inference, reasonable but not documented as BMAD's stated rationale:

  • That "run" specifically inherits from ML experiment tracking (MLflow, W&B) rather than the general computing sense. The structural match — run ID, config, artifact directory — is strong enough that I'd bet on it, but the docs don't say so.
  • That "dimension" carries the spanning/orthogonality intuition deliberately rather than just meaning "aspect."
  • The specific mapping of each mechanism to a named epistemological position in §22 of the main guide. The PR does head a section "Epistemics and reliability," so the intent is explicit; the particular philosophical labels are mine.

Things I have not verified in this session:

  • Every URL in §8. I'm confident about titles and authors; check the links.
  • Exact TOML key paths — confirm against your installed customize.toml.
  • Exact prompt and plan-gate wording, which lives in the skill's reference files.

Where this document and the actual skill disagree, the skill wins. Metaphors are for building intuition, not for settling facts.


Next: bmad-deep-recon-howto.md — every concept here shown as an executable session. Then bmad-deep-recon-guide.md — the full applied guide.

Source: bmad_Research · bmad_research.md · updated 2026-07-23 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

BMAD Research Capabilities — The Complete Reference

Scope & recency. BMAD-METHOD V6 (BMM module), current as of July 2026. The V6 line shipped steadily through spring 2026 (v6.3 → v6.8.x by late May). BMAD moves fast; command names and folder layout shift between point releases, so treat exact command strings as version-dependent and confirm against your install with bmad-help. Where a detail could not be verified against current published docs, it is flagged inline rather than guessed.


Table of Contents

  1. At a Glance
  2. Where Research Sits in the Lifecycle
  3. Choosing the Right Research Type
  4. The Three Research Workflows in Detail
  5. The Sub-Agent Architecture
  6. Research Depth and Scale-Adaptive Intelligence
  7. End-to-End Walkthrough
  8. Scenario Library
  9. Multiple and Parallel Agents (Party Mode)
  10. Where the Facts Come From (Provenance and Retrieval)
  11. Cost, Context, and Running Off-IDE
  12. Brownfield Research and Established Projects
  13. Worked Example: A User Recommendation System
  14. Markov Decision Processes and Stochastic Dependency Graphs
  15. Observations and Findings
  16. Practical Tips and Best Practices
  17. Quick Reference
  18. Glossary
  19. Appendix A: Copy-Paste Research Prompts
  20. Appendix B: Custom Party Definitions
  21. Sources and Recency

1. At a Glance

Research in BMAD is Phase 1 (Analysis) work. It is optional — the required backbone is Planning → Solutioning → Implementation — but it exists to stop you building architecture on unvalidated assumptions. There are three distinct research workflows, all driven by the Analyst agent (persona "Mary"):

WorkflowCommand (short form)Menu codeAnswers
Market Researchbmad-market-researchMRWho else is in this space? TAM/SAM/SOM, competitors, trends, sentiment
Domain Researchbmad-domain-researchDRWhat must I know about this domain? Terminology, regulation, SME depth
Technical Researchbmad-technical-researchTRIs it feasible? Architecture options, library/pattern comparison

Each stands alone — run one, two, or all three. Output is a structured findings document that feeds the PRD, product brief, or architecture workflows.

Command-naming caveat. Current official docs use the short forms above. Some builds and the internal registry use a module-qualified form (bmad-bmm-market-research, etc.). Both refer to the same workflow under bmad/bmm/workflows/1-analysis/research/. If one form isn't recognized, try the other or ask bmad-help.


2. Where Research Sits in the Lifecycle

Phase 1: ANALYSIS (optional)        Phase 2: PLANNING (required)
─────────────────────────           ───────────────────────────
• brainstorming                      • PRD  (bmad-create-prd)
• RESEARCH  ← you are here           • UX design (optional)
    - market
    - domain          ─── findings feed ──▶  the PRD workflow ingests
    - technical                              briefs, PRFAQ, research
• product-brief                              findings, and brainstorm
• prfaq                                       reports as input

The key design idea is that artifacts flow forward. Research findings are not a throwaway chat — they land as documents in your output folder, and the PRD workflow explicitly accepts them as input, synthesizing whatever Phase 1 produced into structured requirements. Running research before planning compounds in value rather than being busywork.

BMM follows a waterfall-with-gates model: each phase produces documents that become context for the next, and mandatory checkpoints (gates) sit before implementation to keep requirements, architecture, and work breakdown aligned.

Default output locations (from config.yaml, set at install time):

  • output_folder_bmad-output (base for everything)
  • planning_artifacts{output_folder}/planning-artifacts
  • project_knowledgedocs (long-lived research / reference docs live here)

So durable research can be steered into docs/, while cycle-specific research lands under planning-artifacts/.


3. Choosing the Right Research Type

The three types answer genuinely different questions. Conflating them is the most common mistake — "research my recommender" is really three separate investigations.

                    What are you actually unsure about?
                                  │
        ┌─────────────────────────┼─────────────────────────┐
        ▼                         ▼                          ▼
  "Does anyone want       "I don't understand        "Can we build it,
   this / who else         this problem space          and how?"
   is doing it?"           / its rules?"
        │                         │                          │
        ▼                         ▼                          ▼
  bmad-market-research     bmad-domain-research      bmad-technical-research
   (MR) — external,         (DR) — vocabulary,        (TR) — feasibility,
   competitive, sizing      constraints, SME depth    architecture, libraries

Rules of thumb:

  • Internal feature for a known audience? Skip market research. Run technical, maybe a light domain pass.
  • Entering an unfamiliar regulated domain? Domain research first — it changes how you'll judge everything downstream.
  • Choosing between concrete technical approaches? Technical research, one focused pass per decision (see §13).
  • Brownfield / existing codebase? Your real entry point is often bmad-document-project, not greenfield research (see §12).
  • Not sure? Run bmad-help and describe the situation; it recommends a starting point based on what you've already produced.

4. The Three Research Workflows in Detail

Market Research (bmad-market-research, MR). Examines competitors, trends, and user sentiment; produces TAM/SAM/SOM sizing, competitive positioning, and persona/journey notes. This is the most mature of the three because it fans out to five specialized sub-agents (§5). Use it to validate that a concept is worth resources before committing.

Domain Research (bmad-domain-research, DR). Builds subject-matter expertise: the terminology practitioners use, industry-specific constraints, regulatory/ethical boundaries, and how "good" is judged in this space. Use it when entering an unfamiliar domain — getting this wrong produces a technically clean system that solves the wrong problem.

Technical Research (bmad-technical-research, TR). Evaluates feasibility, architecture options, and implementation approaches. This is the workhorse for engineering decisions. Note it is currently single-agent (§5) — its output quality depends heavily on how sharply you scope the prompt and what constraints you feed it.

All three are optional Phase-1 workflows run by the Analyst, and all three write findings documents that the PRD workflow can ingest. Run any subset; each stands alone.

Adjacent capability worth knowing — bmad-investigate. V6.8 added a forensic-investigation workflow in the implementation loop that produces evidence-graded findings for debugging and complex system analysis. It isn't one of the three Phase-1 research workflows, but for "why is production behaving like this" questions it's the research-shaped tool you actually want, and it's more grounding-disciplined than a generic research pass.


5. The Sub-Agent Architecture

The research workflow isn't a single monolithic prompt. Market research dispatches to five specialized sub-agents that can work in parallel:

Sub-agentResponsibility
bmm-market-researcherMarket intelligence, TAM/SAM/SOM sizing
bmm-data-analystQuantitative analysis, business metrics
bmm-trend-spotterTrend detection and disruption forecasting
bmm-competitor-analyzerCompetitive positioning
bmm-user-researcherPersonas and journey mapping

The known gap

As of the V6 line, technical research has NO dedicated sub-agents (tracked upstream as a feature request). When you run technical/architecture research, the main Analyst agent executes queries sequentially by itself. Consequences:

  • Slower — no parallel fan-out the way market research gets.
  • Shallower specialization — no dedicated "architecture evaluator" or "library comparator."
  • You carry more of the load — quality depends on how sharply you scope the prompt and how much source material / constraints you supply up front.

The single most useful thing to internalize before relying on bmad-technical-research for anything load-bearing: it is a well-structured single-agent research pass, not a multi-agent swarm. Treat it accordingly, and use Party Mode (§9) when you need genuine multi-perspective technical research.


6. Research Depth and Scale-Adaptive Intelligence

Step 5 of the workflow asks you to set research depth — a knob controlling how much source-gathering and cross-checking happens versus a fast synthesis pass. Deeper settings mean more web search, more sources, longer runs, and higher token cost.

Honest, still-open gap. The exact depth option values (e.g. quick/standard/deep vs. numeric levels) are defined in the workflow's step files, not the public docs, and could not be verified at time of writing. Don't hard-code a value from memory; when the workflow prompts, read the options it offers, or ask bmad-help what your installed version exposes.

Scale-Adaptive Intelligence is the related, documented system: bmad-help analyzes project-complexity indicators (team size, architectural complexity, compliance requirements) and recommends appropriate phase entry points and planning depth. It's how BMAD scales from a five-minute bug fix (Quick Flow) to enterprise-platform planning without forcing heavy process on small work. The three planning tracks — Quick Flow, BMad Method, Enterprise — are the concrete expression of this and each defines which workflows are required, optional, or conditional.


7. End-to-End Walkthrough

The workflow follows five interactive steps. Here's a full run for a technical decision.

Step 0 — Fresh chat. Always start research in a new chat. Stale context from an unrelated workflow degrades quality and costs more tokens.

Step 1 — Load the Analyst.

bmad-agent-analyst

(You can also invoke the workflow skill directly and let the tool route to the Analyst; loading the agent first gives you the conversational menu with the MR/DR/TR triggers.)

Step 2 — Run the workflow.

bmad-technical-research

Step 3 — Choose research type. Since you invoked the technical variant directly, this is already TR.

Step 4 — Provide context. This is where the run is won or lost (especially given no technical sub-agents). Give a tight problem statement, the constraints, and what a good answer looks like. See Appendix A for ready-to-use prompt shapes.

Step 5 — Set research depth. Pick depth by stakes. A "which library" spike can be shallow; an architecture bet that shapes service topology warrants the deepest setting.

What comes out. A structured findings document in your output folder — typically: options considered, comparison across your constraints, tradeoffs, a recommendation, and (for technical research) a sketch of a validation/PoC path. Because it's a file, it becomes an input you hand straight to bmad-create-architecture later.

What happens next automatically. The workflow ends by invoking bmad-help, which inspects project state and suggests the next step (usually product-brief or PRD). If you treat BMAD as a personal tool, ignore the suggestion and keep the findings doc.


8. Scenario Library

Scenario A — Technical research (feasibility spike)

Situation: Deciding whether pre-execution validation of Athena queries should be a hand-rolled parser, a sqlglot-based validator, or Athena's own dry-run / EXPLAIN path. Expected findings: a tradeoff matrix (safety coverage vs. cost vs. language fit), a recommended defense-in-depth layering, and which failure modes each layer catches. Observation: with no technical sub-agent, expect to iterate — feed back the option you're leaning toward and ask it to pressure-test that specifically.

Scenario B — Domain research (unfamiliar space)

Situation: Scoping a recommendation feature inside a legal-tech product; you want the domain landscape before writing a PRD. Expected findings: a subject-matter primer — vocabulary, the constraints that make the domain different (authority, jurisdiction, recency of law), and the evaluation lens you'd otherwise miss. Why domain, not technical: you're building framing, not choosing a library.

Scenario C — Market research (validation before build)

Situation: Deciding whether an internal demo is worth productizing. Expected findings: this is where the five sub-agents earn their keep — a competitor map, sizing, trend forecast, and persona notes assembled in parallel, then synthesized.

Full prompt text for each is in Appendix A.


9. Multiple and Parallel Agents (Party Mode)

There are two distinct forms of "multi-agent" in BMAD, and conflating them is the most common mistake:

  1. Built-in sub-agent fan-out — automatic, inside a single workflow. Market research already does this (§5). You don't control it directly.
  2. Party Modeyou convene multiple agent personas in one room to attack a question from different angles. This is the deliberate, steerable tool.

Reality check on "parallel." BMAD agents are registered as Skills, which run synchronously in the main chat thread. Even Party Mode is a simulated round-table, not OS-level concurrency — there's an open request to wrap agents as native IDE subagents for true async execution, but that isn't the default. Read "parallel" as "independent reasoning per persona," not "N agents at the same wall-clock instant." The exception is agent-team mode (Claude Code only), which stands personas up as a persistent team.

9.1 The four Party Mode modes (who does the thinking)

Invoke with bmad-party-mode; pick a mode with --mode:

ModeWhat it doesReach for it whenCost
sessionDefault. One model voices every persona inline.Banter, brainstorming, quick back-and-forth.Cheapest
autoVoices inline for light rounds; spawns independent agents only when independence changes the answer.Speed most of the time, independence on hard rounds.Medium
subagentSpawns a separate agent per persona every substantive round.Honest reviews and focus groups — voices must not bleed together.High
agent-teamPersonas stand up as a persistent team addressing each other directly. Claude Code only.A live, hands-off round-table.Highest

Fallback chain: agent-teamsubagentsession when a harness can't do the rest. On a non-Claude-Code tool, asking for agent-team quietly degrades.

The core principle: one model voicing five personas will quietly converge — they share a mind, so they tend to agree. Spawning real agents (subagent/agent-team) keeps reasoning genuinely separate, which is the entire point of a review panel or focus group. If you use multi-agent for diverse input and run it in session, you paid for theater.

9.2 ⚠️ Critical best practice: grounding vs. fabrication

A known V6.3 regression worth internalizing before trusting subagent mode on anything codebase-grounded: the default subagent spawn prompt ended with an instruction telling each persona not to use tools — "just respond with your perspective." On a codebase-grounded question, spawned agents returned zero tool calls and fabricated a meaningful fraction of their claims (roughly a third in one reported run on a ~1000-line file), because they reasoned from the orchestrator's lossy summary instead of reading the source. In the same tests, --solo/session and tool-enabled variants stayed grounded.

Rules that follow:

  • For grounded work (real code, real data), verify the spawned personas can actually read the sources. If the spawn prompt forbids tools, the panel is opining on a summary, not the artifact.
  • Party Mode shines for judgment questions, not fact-retrieval. "Which reranking approach fits our constraints?" is perfect. "What does line 340 do?" is exactly where the summarization gap bites.
  • When in doubt, feed the source into the room yourself (paste the diff, metrics, findings docs) rather than trusting each subagent to fetch it.
  • Confirm on your version — this specific prompt has been iterated upstream, so newer builds may already ground subagents.

9.3 The shipped review panels

BMAD ships two custom parties as working templates (defined but inactive until summoned — they cost nothing sitting in the pool):

Code Review Crew — five lenses that attack a change and argue about what matters: Vex (security/exploit path), Grumbal (adversary, assumes it's broken), Boundary (edge cases, nulls, races, timezones), Yui (craft, simplicity, naming), Dana (pragmatist, ranks real vs. nit). Run with subagent so each reviews before they clash.

Anti-Consensus Club — for decisions and fuzzy questions where one assistant agrees too fast or debates past the useful point: Wildcard (alternative framings), Level (claim/confidence checker), Killjoy (stops repetition and fake disagreement), Splinter (challenges easy consensus). Run with --party anti-consensus-club --mode subagent.

9.4 Working around the technical-research sub-agent gap

Because bmad-technical-research is single-agent (§5), Party Mode is the sanctioned way to get multi-perspective technical research. Two patterns:

Pattern A — Fan out, then convene. Run several separate technical-research chats, one per candidate approach (fresh chat each), producing one findings doc apiece. Then open a Party Mode room in subagent mode, feed in all the findings docs, and let a purpose-built panel reconcile them. Depth (focused single-agent research per option) and independent cross-examination, without the missing built-in sub-agents.

Pattern B — Build a custom research panel via bmad-party-mode, create a new party (writes to your overrides through bmad-customize). Ready-to-use definitions are in Appendix B.

9.5 Steering, memory, and keepsakes

  • Steer actively: bring a voice in, tell one persona to "take that apart," switch rooms mid-session, or summon a named custom member.
  • 2–3 active voices per round, not all at once — more turns to noise.
  • Non-interactive: --non-interactive "…" runs one pass to a close and releases spawned agents (good for scripted use).
  • Keepsake: on wrap-up the orchestrator offers a self-contained HTML document laid out by persona — a genuinely useful artifact to attach to a ticket.
  • Memory: saved parties keep lightweight memory of past sessions (dynamics, open threads) unless you turn it off via bmad-customize. Good for a standing panel; off for one-offs you don't want coloring later runs.

10. Where the Facts Come From (Provenance and Retrieval)

The question a technical reader asks first and most docs skip: when a research workflow states a "fact," where did it come from?

BMAD ships no crawler, search index, or retrieval engine. The research workflows are orchestration — structured prompts, step files, personas, output templates. The actual source-gathering rides on the host coding agent's tool access. In Claude Code that means the model's web search, any MCP connectors you've enabled, and file/repo reads. In a locked-down harness with no web tool, the same workflow falls back to the model's parametric knowledge — cutoff-bound, potentially stale or confabulated.

Consequences:

  • Research currency is bounded by your host's tools. Market/domain research is only as fresh as the web access behind it.
  • Technical research in a brownfield repo sources mostly from your own code — so the host needs file-read access, and (per §9.2) spawned subagents need tool access or they hallucinate.
  • Provenance hygiene is on you. Ask the workflow to cite what it actually fetched; treat uncited claims as model recall, not verified fact. Matters most for bmad-technical-research, where a confidently-wrong claim about a library costs you a bad architecture bet.
  • Working memory: a memlog primitive (_bmad/scripts/memlog.py) carries state across steps within a run, so a multi-step pass doesn't drag the whole context forward.

The honest framing: BMAD makes research structured and repeatable; it does not make it grounded — grounding is a property of the tools you point it at.


11. Cost, Context, and Running Off-IDE

Token cost is real. Independent testing has put a full planning cycle in the rough ballpark of ~$200 in tokens (an order-of-magnitude signal, not an official figure). Two knobs multiply it:

  • Depth (§6) — deeper = more sources, more cross-checking, more tokens.
  • Party Mode spawning (§9.1) — subagent/agent-team spin up a separate agent per persona per round, so an N-persona panel is roughly N× the per-round cost of session. Great for independence, expensive by design.

Context management keeps this affordable. BMAD's step-file architecture loads workflow steps just-in-time rather than holding the whole workflow in context, and each SKILL.md uses YAML frontmatter to persist runtime state across turns. The fresh-chat-per-workflow discipline (§7, §9.5) isn't just a quality rule — it's a cost rule; stale accumulated context is both worse and pricier. memlog carries the few things worth remembering so a fresh chat isn't a cold start.

Web Bundles (v1.0, May 2026) are the token-conscious escape hatch. BMAD can package planning/research skills — market research, brainstorming, product briefs, PRFAQ, PRD, UX specs — as Google Gemini Gems and ChatGPT Custom GPTs. Run the research-and-planning-heavy front of the process on a flat-rate web subscription instead of metered IDE tokens, then bring artifacts back into the IDE for solutioning and implementation. For token-conscious use this is the single biggest lever: offload the exploratory, high-token research phase to a fixed-cost surface.

Customization. Research workflows are customizable like every BMad skill via bmad-customize (writing to your overrides). Pin a default depth, set house rules the workflow holds for the whole run, add persistent facts so you don't re-explain your stack every time. The clean, version-safe way to shape behavior without forking the workflow.


12. Brownfield Research and Established Projects

Most scenarios frame research as pre-build validation, but in an existing codebase your real research entry points differ.

bmad-document-project (DP, Analyst) reverse-documents an existing codebase into reference docs. Critically, its output can substitute for Phase 1–2 artifacts, letting a brownfield project enter at Phase 3 (Solutioning) with generated context. For a live system, this is often the actual first move — not greenfield market/domain research.

bmad-generate-project-context (GPC, Analyst) produces project-context.md, which acts as the "constitution" for your project: technical preferences and implementation rules every agent must follow. You can hand-write it at the project root or in _bmad-output/, or auto-generate it by scanning the codebase. For research specifically, a good project-context.md means technical-research passes start already knowing your stack, conventions, and constraints — less re-explaining, sharper findings.

Practical brownfield stance:

  • Market/domain research is frequently skippable for internal brownfield work — don't run the full Phase-1 suite out of completeness reflex.
  • The useful pair is usually document-project (understand the system) + technical-research (decide one thing), optionally with bmad-investigate (§4) when the question is "why is it behaving like this."
  • Feed project-context.md into research runs so findings respect your actual constraints from the first token.

13. Worked Example: A User Recommendation System

This section shows how BMAD research actually works on a real build. The feature: a user recommendation system that suggests content based on a user's view history. It's the ideal case study because it spans all three things research handles — algorithm choice, failure modes, and infrastructure — and because it exposes the hard boundary between what BMAD researches and what your data has to decide.

Framing first (the whole point). A recommender is ~90% technical research, ~10% domain, 0% market (internal feature, not a product to position). And per §10, BMAD will structure and survey the problem — it will not benchmark EMA-vs-two-tower on your click logs. Everything below is decision-support; the empirical answers come from eval, not the research workflow (see §13.7).

13.1 Decompose the feature into research passes

Don't run one giant "research a recommender" prompt — it produces a shallow essay. Split into focused technical-research passes, each a real decision:

PassDecision under researchWorkflow
A — Scoring & representationHow do we model taste and score candidates? (EMA profiles, vectorization, hybrid)bmad-technical-research
B — Failure modesWhat goes wrong and how do we defend? (duplicates, cold start, popularity bias, staleness, feedback loops)bmad-technical-research
C — Architecture & deploymentHow does this run on AWS within latency/cost/DR constraints?bmad-technical-research
D (light) — DomainHow is "relevant" judged in this content domain?bmad-domain-research

Then reconcile the technical passes in a Party Mode panel (§13.5) — the fan-out-then-convene pattern from §9.4, and the way to work around the missing built-in technical sub-agents.

13.2 Pass A — scoring & representation

Real question: how do you turn "what a user viewed" into "what to show next," and keep it fresh without recomputing everything?

Options to compare and the substance you're validating:

  • EMA user-preference vector — maintain u ← α·e_item + (1−α)·u per interaction (or time-decayed weight = exp(−λ·Δt)). α (or a half-life) is the recency knob. Cheap, updates online in O(1), naturally forgets stale interests. Known weakness: interest blending — a user who reads both tax law and IP law collapses into a muddy centroid that recommends neither well. That single fact is usually what pushes toward the next option.
  • Recent-k / multi-vector — keep the last k interaction embeddings and retrieve by max-similarity (late-interaction style) instead of a single centroid; preserves distinct interests at higher retrieval cost. (Where ColBERT / MUVERA / late-interaction thinking connects.)
  • Two-tower learned model — user tower + item tower trained on engagement; best relevance ceiling but adds training infrastructure and a cold-start dependency on engagement data.
  • Hybrid — content-based ANN candidate generation (works cold) + collaborative signal in rerank once engagement exists. The usual pragmatic landing spot.

Retrieval mechanics: content/item embeddings in OpenSearch k-NN (HNSW); the user vector (EMA aggregate or multi-vector) drives an ANN query for the candidate set. Dimensionality and quantization (PQ / IVF-PQ) are the recall-vs-cost knobs.

Observation: research lays out this tradeoff space cleanly. It cannot tell you the right α or whether EMA is "good enough" for your users — that's §13.7.

13.3 Pass B — the failure modes (where recommenders actually break)

Duplicates are the headline, but they're one of a family. A good findings doc gives a layered defense and says where each layer sits and what it misses:

Failure modeMitigationPipeline stage
Exact duplicates (same doc resurfaced)Seen-set exclusion against view history (e.g. your Athena history)Filter
Near-duplicates (same story, diff source; re-embeds)Cosine-similarity dedup or collapse by an OpenSearch cluster field with top_hitsFilter / candidate-gen
No diversity (five near-identical items)MMR rerank: score = λ·rel − (1−λ)·max_sim_to_selectedRerank
Cross-session repetition (already-seen items)Impression discounting — decay recently-shown-not-clicked, don't hard-excludeRerank / post
Popularity bias (head drowns the tail)Inverse-propensity weighting / popularity normalizationScoring
Cold start (new user / new content)Content-based ANN fallback + popularity/editorial priorCandidate-gen
Staleness / filter bubbleExploration — ε-greedy or bandit slot injecting off-profile candidatesPost

The layering insight research should surface: these are ordered stages (candidate-gen → seen-filter → dedup/cluster-collapse → MMR/diversity → impression-discount), and each layer only catches its own class — MMR won't catch exact dupes, seen-set won't catch near-dupes. That ordering is the finding. (The exploration slot in the last row is a one-step special case of a fuller sequential formulation — see §14 for modeling the recommender as a Markov Decision Process.)

13.4 Pass C — AWS architecture & deployment

Real question: how does all of the above run in production within latency, cost, and DR constraints? What good findings look like:

  • Two paths. A batch/offline path (embed the corpus, backfill the vector index, precompute where possible) and an online path (per-request retrieve → filter → rerank). Keeping heavy work off the request path is the p95 story.
  • State placement. EMA user vectors in DynamoDB (fast point read on the request, O(1) write on interaction, TTL to forget dormant users); content vectors in OpenSearch k-NN; a recommendation Lambda orchestrates retrieve → seen-filter → dedup → MMR rerank → Content API hydrate.
  • Embedding generation. Batch backfill (SageMaker Batch Transform / Bedrock) for the corpus; near-real-time for newly ingested content via a Lambda on the ingest event.
  • Latency budget is spent mostly in ANN retrieval + rerank; MMR over a bounded candidate set is cheap, a cross-encoder rerank is where you'd blow the budget (ties to Scenario A / reranking decisions).
  • Edge & DR. CloudFront + ALB in front; origin-layer DR determines what happens to the rec call when a region degrades — fail to a popularity/editorial fallback rather than erroring.
  • Top cost levers. Vector quantization (PQ) vs. recall, embedding-recompute frequency, and DynamoDB read/write capacity mode.

13.5 Reconcile the passes in a panel

Use §9. Open a Party Mode room in subagent mode and feed in the three findings docs — because the passes interact (a multi-vector user model changes ANN cost, which changes the AWS budget, which changes whether a cross-encoder rerank is affordable):

bmad-party-mode --mode subagent
# panel: relevance-engineer, latency-hawk, ops-SRE, cost-owner, eval-skeptic
# input: pass-A-scoring.md, pass-B-failure-modes.md, pass-C-aws.md

The eval-skeptic persona matters most here — its job is to keep asking "how will we know this is better?", forcing the design toward §13.7. (Heed §9.2: make sure the panel can actually read the three docs.)

13.6 What chains downstream

The three findings docs + the panel keepsake feed:

  • bmad-create-prd — synthesizes them into requirements (it ingests research findings as input, §2).
  • bmad-create-architecture — Pass C becomes the architecture backbone; Passes A/B become the algorithm and quality sections.
  • The eval plan — where the real work starts.

13.7 The honest boundary — what the research cannot tell you

This is the "how it actually works here" punchline. BMAD technical research produces a decision-support package: options, tradeoffs, a layered design, cited sources (only if the host has web access — §10), and a proposed evaluation plan.

It does not, and cannot, tell you:

  • the right α / half-life for your EMA,
  • the right cosine threshold for near-dup collapse or λ for MMR,
  • whether EMA beats two-tower on your users,
  • your actual p95 under load.

Those are empirical and belong to eval-driven development, not research — adversarial multi-evaluator loops, offline replay against view history, and online A/B. The division of labor:

Research designs the experiment and narrows the option space. Your data runs the experiment and picks the winner.

A recommender is precisely where teams mistake a confident research doc for an answer and skip the eval. The research is real and valuable — it stops you building the wrong thing — but the tuning constants that decide whether the feature is good come from measurement, and the research's job ends at handing you a sound experiment to run.


14. Markov Decision Processes and Stochastic Dependency Graphs

14.1 Does BMAD support this natively? No — and it's worth being precise about why

BMAD's dependency model is a deterministic directed graph with conditional branches and mandatory gates — the graph TD planning flow (idea → optional research → brief → PRD → architecture → gates → implementation), soft preceded-by/followed-by sequencing in module-help.csv, and scale-adaptive heuristic routing to a track (Quick Flow / BMad Method / Enterprise). There is:

  • no transition probability between milestones (a gate is pass/fail by review, not P(pass | state, action)),
  • no reward/value function or policy being optimized,
  • no MDP/POMDP solver, no stochastic scheduler, no Bayesian dependency graph.

"Scale-adaptive intelligence" and bmad-investigate's "evidence-graded findings" sound probabilistic but are heuristic/qualitative grading, not a Markov model. So if the question is "can I hand BMAD a stochastic milestone graph and have it solve for an optimal policy" — no, that's outside its design.

That said, MDPs and stochastic dependency graphs are relevant to your work in two distinct, genuinely useful ways. Neither is a built-in feature; one is a research subject BMAD is well-suited to investigate, the other is an overlay you build on top of BMAD's deterministic graph.

14.2 The concept — what a Markov Decision Process actually is

An MDP is the standard mathematical model for sequential decision-making under uncertainty: an agent repeatedly observes a state, picks an action, receives a reward, and lands (probabilistically) in a new state — and it wants to choose actions that maximize reward over the long run, not just right now.

The Markov property. The defining assumption: the future depends only on the current state, not the whole history — P(s_{t+1} | s_t, a_t, s_{t-1}, …) = P(s_{t+1} | s_t, a_t). In practice you engineer the state to make this true. If recent history matters (it does for a recommender), fold it into the state — and note an EMA vector already summarizes history into one carry-forward number, which is exactly a Markov-friendly representation.

The five pieces ⟨S, A, P, R, γ⟩.

  • S — the set of states the world can be in.
  • A — the actions available.
  • P(s′ | s, a) — the transition model: the probability of the next state given the current state and action. The stochastic heart of the thing.
  • R(s, a) (or R(s,a,s′)) — the reward for taking an action.
  • γ ∈ [0,1) — the discount factor: a reward t steps away is worth γ^t of face value. γ near 0 = myopic (only now matters); γ near 1 = far-sighted.

What "solving" an MDP means: finding a policy. A policy π(a | s) maps states to actions. The quantity you care about is the expected discounted return from following π:

V^π(s) = E[ r_0 + γ·r_1 + γ²·r_2 + …  | start in s, follow π ]

V^π(s) is the state-value function; Q^π(s,a) is the action-value function (value of taking a in s, then following π). Solving the MDP = finding π* that maximizes value in every state.

The Bellman equation is the recursion that makes this tractable — value is immediate reward plus discounted value of where you land:

V*(s)   = max_a [ R(s,a) + γ · Σ_{s′} P(s′|s,a) · V*(s′) ]
Q*(s,a) =         R(s,a) + γ · Σ_{s′} P(s′|s,a) · max_{a′} Q*(s′,a′)

How you actually solve it — two worlds:

  1. Model known (you have P and R). Use dynamic programming: value iteration (sweep the Bellman update to convergence) or policy iteration (alternate evaluate-policy / improve-policy). This is the milestone graph of §14.4 — few states, estimable transitions, solvable in a few lines.
  2. Model unknown (you only get to act and observe). This is reinforcement learning. Q-learning updates value estimates from sampled transitions:
    Q(s,a) ← Q(s,a) + α·[ r + γ·max_{a′} Q(s′,a′) − Q(s,a) ]
    
    Policy-gradient methods instead push π directly toward higher return. This is the recommender of §14.3 — you can't write down how a user will respond, so you learn a policy from interaction logs.

Exploration vs. exploitation. With an unknown model, the agent must choose between exploiting the action that looks best now and exploring an uncertain one that might be better and teaches you something. ε-greedy, UCB, and Thompson sampling are the standard strategies — and this tension is exactly the diversity/exploration knob from §13.3, now with a name and a theory.

POMDP — when you can't see the true state. If the agent only gets a noisy observation rather than the state itself, it's a Partially Observable MDP. Almost every real system is one: you never observe a user's true intent, only their clicks. The fix is a belief state (a summary of history — again, the EMA vector is a crude one) used in place of S.

Two intuition anchors before we apply it:

  • The bandit is the smallest MDP. A multi-armed bandit is an MDP with one state and γ = 0 — pure "which action pays best right now," no sequential effect. A contextual bandit adds a state (context) but still γ = 0. Set γ > 0 and let actions change the next state and you have a full MDP. That ladder (bandit → contextual bandit → MDP/RL) is the natural adoption path.
  • Discounting is a modeling choice, not a detail. γ encodes how much you care about the future — it's where product intent enters the math.

14.3 Applying it #1 — the recommender as an MDP (a research subject, not a BMAD feature)

The strong fit, extending §13 directly. A recommender is a textbook (PO)MDP: the user is the environment, and each recommendation nudges the user into a new state whose response you can't fully predict.

The tuple ⟨S, A, P, R, γ⟩ instantiated:

ElementRecommender meaning
S — stateUser context: the EMA/user vector + session features + recently-shown impressions + time-of-day, etc.
A — actionWhich item or slate to recommend now (slate MDPs are the multi-item case).
P(s′|s,a) — transitionThe stochastic part: how the user's next state depends on what you showed — click, skip, dwell, or churn. The "stochastic dependency" between interaction states.
R(s,a) — rewardEngagement signal: click, dwell time, or a longer-horizon retention/return signal.
γ — discountHow much you weight future satisfaction vs. the immediate click. γ > 0 is the whole reason to bother — it discourages clickbait and filter-bubble myopia.

Myopic vs. sequential — the concrete difference. A greedy ranker (your EMA+MMR design) maximizes immediate reward: show the highest-predicted-click item now. That can quietly degrade the state — hammer someone with the same high-CTR category and they narrow, get bored, and eventually churn. An MDP with γ > 0 will sometimes show a slightly lower-CTR item because it improves the future state — it discovers a new interest, keeps the session alive, protects long-term retention. Same catalog, same infra, different objective: sequence-value instead of click-value. That is the entire reason to reach for an MDP here.

It's really a POMDP, and that's good news for reuse: you never see true intent, but the belief state you can build is the EMA/user vector from §13. So MDP/RL layers on top of §13's representation and serving rather than replacing it — the user vector becomes (part of) S, the ANN candidate set constrains A, and the ranker becomes a learned policy.

The adoption ladder (crawl → walk → run):

  1. Contextual bandit (γ = 0) over the rerank slot — the smallest useful step, and a principled version of the ε-greedy exploration you already flagged.
  2. Slate bandit / slate MDP — handle that you recommend a set, where items interact (diversity, position bias); SlateQ-style decomposition tames the combinatorial action space.
  3. Full RL with γ > 0 optimizing a long-horizon reward (retention, return visits) — highest ceiling, highest cost.

The hard part is evaluation, not the algorithm — off-policy evaluation (OPE). You can't A/B-test every candidate policy on live users. OPE estimates a new policy's value from logged data collected under the current policy, via inverse-propensity scoring (IPS) and doubly-robust estimators. This is the practical gate on RL recommenders and the thing a research pass should design before anyone trains a policy.

Pitfalls research should surface (all real, all expensive to learn late):

  • Reward shaping / gaming — optimize raw dwell and you breed clickbait; optimize clicks and you breed rage-bait. The reward is the product's values.
  • Credit assignment / delayed reward — retention arrives days later; attributing it to a recommendation three sessions ago is genuinely hard.
  • Feedback loops — the policy shapes the data that trains the next policy; unchecked, it collapses diversity (the filter bubble, now self-reinforcing).
  • Serving & stability — a learned policy in the request path adds latency, a model-serving dependency, and the risk of a bad policy reaching users.

How BMAD fits: research it with a technical-research pass (prompt A8 in Appendix A) — "MDP/RL vs. heuristic, and if RL, what's the OPE plan and the crawl-walk-run path?" The §13.7 boundary applies with extra force: whether RL beats EMA+MMR on your data is empirical, and the cost of being wrong is higher. Research defines the experiment (state/action/reward design, the OPE methodology, rollout guardrails); your logs decide whether to climb the ladder.

14.4 Applying it #2 — modeling BMAD's own milestones as a stochastic graph (an overlay you build)

The literal reading of the question — a stochastic dependency graph between milestones — and where the model-known branch of §14.2 applies. BMAD gives you the graph structure for free (phases, gates, and the preceded-by/followed-by edges in module-help.csv); you supply the probabilities and solve it outside BMAD.

Two versions, increasing power:

  • Markov chain (no decisions) — model gate outcomes as random transitions to estimate schedule risk; this is essentially stochastic PERT.
  • MDP (with decisions) — add actions at each gate — {proceed, iterate/rework, cut scope, run more research} — and solve for the optimal policy: given where the plan is and how risky the next gate looks, what's the cost-minimizing move?

A worked example. Model each gate as a transition over {pass, rework}:

              p_pass = 0.7
   [Arch gate] ───────────────▶ [Implementation]
       │  ▲
p_rework│  │ 0.3   (loop back, pay rework cost c_r)
       ▼  │
   [Re-architect]
  • Expected attempts to clear a gate with pass-probability p is geometric: 1/p. At p = 0.7 that's ≈ 1.43 attempts; at p = 0.5 it's 2.0. Each extra attempt costs c_r (rework time).
  • Chain three independent gates at p = 0.7: P(clean run) = 0.7³ ≈ 0.34 — only a third of projects clear all three without rework. That number alone reframes how you plan.
  • Expected total rework ≈ Σ_gates (1/p_g − 1)·c_r — a concrete, defensible schedule-risk figure.

The elegant payoff — value of information. Treat "run a technical-research pass" as an action that changes a transition probability: it raises the architecture gate's p_pass (fewer late re-architecture loops). Then:

value of the research  ≈  (expected rework cost WITHOUT research)
                        − (expected rework cost WITH research)
research is worth it   ⇔   that difference  >  the research's own cost

If a research pass lifts the architecture gate from p = 0.5 to p = 0.7, expected attempts drop from 2.0 to ≈ 1.43 — saving ≈ 0.57 rework cycles at that gate. If one rework cycle costs more than the research pass, research pays for itself. This turns §13.7's qualitative "research stops you building the wrong thing" into a decision-theoretic ROI — the value-of-information calculation at the heart of MDP planning.

How to solve it: the graph is tiny (a handful of states), so value iteration by hand or ~20 lines of NumPy yields the optimal gate policy; pure schedule risk is a PERT / Monte-Carlo run.

What BMAD does / doesn't do: it emits the milestone graph and the artifacts; it does not estimate the probabilities or solve anything. You pull the edges (from module-help.csv or the planning flow), attach p values from your own historical delivery data (the weak link — garbage in, garbage out), and solve it in a notebook. A tidy loop: use bmad-technical-research to design the risk model, then own it yourself. Keep the honest caveat: the map is not the territory — a clean expected-value number built on three guessed probabilities is worse than no number.

14.5 Honest summary

  • Native BMAD support for MDP / stochastic dependency graphs: none. Its model is a deterministic gated DAG with heuristic routing.
  • The concept (§14.2) is sequential decision-making under uncertainty — ⟨S,A,P,R,γ⟩, solved for a policy via dynamic programming (model known) or RL (model unknown), with exploration-vs-exploitation and partial observability as the two complications that always show up in practice.
  • MDP as a recommender technique (§14.3): fully applicable — the sequential, long-horizon generalization of the greedy ranker in §13, a natural bmad-technical-research subject, with off-policy evaluation as the real gate.
  • Stochastic graph over BMAD's own milestones (§14.4): a valid overlay you build on the graph BMAD gives you — useful for quantifying plan risk and computing the value-of-information ROI of research itself, but solved outside BMAD.

15. Observations and Findings

Strengths

  • Artifact-first design is the real value. Research isn't a chat you lose — it's a document the PRD/architecture workflows consume. The compounding across phases is the point.
  • Market research is genuinely multi-agent and produces broad, structured coverage with little hand-holding.
  • Clean separation of the three types maps to actually-different questions and stops you conflating "is it buildable" with "does anyone want it."
  • Scale-adaptive: depth adjusts to project complexity, so bug-fix-sized work isn't pushed through enterprise-weight research.

Weaknesses / watch-outs

  • Technical research is single-agent — weakest of the three for depth and speed. For high-stakes bets, plan to iterate and supply strong constraints. It amplifies a sharp prompt; it won't out-think a vague one.
  • Depth options are opaque across versions — confirm at runtime.
  • Command drift between short (bmad-*) and module-qualified (bmad-bmm-*) forms; bmad-help is the source of truth.
  • subagent grounding regression (§9.2) — verify tool access before trusting a panel on codebase-grounded questions.
  • Fresh-chat discipline is load-bearing — the most common cause of degraded, costlier output.
  • Grounding is not guaranteed (§10) — research is only as current as the host's tools.

Complementary modules

  • Creative Intelligence Suite (cis) — facilitated ideation frameworks (SCAMPER, reverse brainstorming, reframing) for before you have a crisp question to research.
  • BMad Builder (bmb) — author your own .agent.yaml sub-agents / workflows; the sanctioned path to a personal "architecture-evaluator" sub-agent that closes the technical-research gap.
  • Test Architect (tea) — risk-based test strategy and NFR assessment; research-adjacent when the question is "how will we validate this."

16. Practical Tips and Best Practices

  1. For technical research, over-specify constraints. Latency budgets, runtime fit, "no new platform," existing infra you must reuse. The single agent leans on your framing.
  2. State the decision, not just the topic. "Which of A/B/C and why" produces a usable doc; "tell me about reranking" produces an essay.
  3. One decision per pass. Fan out across passes, then convene a panel (§9.4).
  4. Match Party Mode mode to intentsession for ideation, subagent for honest critique/focus groups, agent-team for hands-off debate.
  5. Verify subagent grounding before trusting a panel on real code/data (§9.2).
  6. Route durable research to docs/, cycle-specific to planning-artifacts/.
  7. Feed project-context.md into research so findings respect your stack from the first token (§12).
  8. Offload to Web Bundles when tokens matter — run the heavy research phase on a flat-rate GPT, bring artifacts back (§11).
  9. Take the keepsake HTML and attach it to the ticket; it's a real artifact.
  10. Remember the boundary — research designs the experiment; your data picks the winner (§13.7).
  11. Use bmb to close the sub-agent gap if you do technical research often on the same stack.
  12. Always fresh chat per workflow — quality and cost both depend on it.

17. Quick Reference

Research commands

# Fresh chat, then:
bmad-agent-analyst          # load Analyst (Mary); exposes MR / DR / TR

bmad-market-research        # MR — competitors, sizing, trends (5 sub-agents)
bmad-domain-research        # DR — terminology, regulation, SME depth
bmad-technical-research     # TR — feasibility, architecture, libs (single-agent)

bmad-help                   # source of truth for what's installed / what's next

Research-adjacent

bmad-document-project           # DP  — reverse-document an existing codebase (brownfield)
bmad-generate-project-context   # GPC — write project-context.md ("constitution")
bmad-investigate                # forensic, evidence-graded system analysis (v6.8)
bmad-brainstorming              # guided ideation (before research)
bmad-party-mode                 # multi-persona room (--mode session|auto|subagent|agent-team)
bmad-customize <skill>          # pin defaults, house rules, persistent facts

BMM agents (persona · key menu codes)

AgentPersonaCodes
AnalystMaryBP, CB, MR, DR, TR
Product ManagerJohnCP, VP, EP, CE, IR, CC
ArchitectWinstonCA, IR
Scrum MasterBobSP, CS, ER, CC
DeveloperAmeliaDS, CR
QA EngineerQuinnQA
UX DesignerSallyCU
Technical WriterPaigeWD, MG, VD, EC
Quick Flow Solo DevBarryQS, QD, CR

Output_bmad-output/planning-artifacts/ (cycle) or docs/ (durable). Feedsbmad-create-prd, bmad-product-brief, bmad-create-architecture.


18. Glossary

BMAD terms

  • BMM — BMad Method module; the flagship four-phase agile-AI module.
  • Analyst (Mary) — the agent that runs all three research workflows.
  • Skill — a workflow packaged as a SKILL.md + sharded step files; runs synchronously in the host IDE.
  • Step filestep-XX-*.md, loaded just-in-time to keep context lean.
  • module-help.csv — catalog mapping skills to names, menu codes, phases, output locations; powers bmad-help next-step routing.
  • Party Mode — multi-persona conversation; four modes govern independence.
  • Sub-agent — a specialized agent spawned within a workflow (market research) or a party (subagent mode).
  • Scale-Adaptive Intelligence — automatic planning-depth adjustment by project complexity; expressed as Quick Flow / BMad Method / Enterprise tracks.
  • project-context.md — the project "constitution": rules all agents follow.
  • memlog — working-memory primitive carrying state across steps in a run.
  • Web Bundles — planning skills packaged as Gemini Gems / ChatGPT GPTs for flat-rate, off-IDE planning.
  • Gate — a mandatory checkpoint before advancing phases.

Recommender terms (used in §13)

  • EMA — exponential moving average; u ← α·e + (1−α)·u for recency-weighted user profiles.
  • Half-life / α / λ — the recency-decay knobs (interaction-count or time-based).
  • Vectorization / embedding — mapping content/users to vectors for similarity search.
  • ANN / HNSW / IVF-PQ / PQ — approximate nearest-neighbor search and its index/quantization variants (recall-vs-cost).
  • Two-tower — separate learned encoders for users and items.
  • MMR — Maximal Marginal Relevance; diversity rerank trading relevance against redundancy.
  • Impression discounting — down-weighting recently-shown-not-clicked items.
  • Inverse-propensity weighting — correcting popularity bias.
  • Cold start — no history for a new user/item.
  • Eval-driven development — deciding tuning constants by measurement (offline replay, adversarial evaluators, online A/B), not by research.

MDP and stochastic-planning terms (used in §14)

  • MDP — Markov Decision Process; ⟨states, actions, transition probabilities, reward, discount γ⟩ for sequential decisions under uncertainty.
  • Markov property — the future depends only on the current state, not the full history; you engineer the state to make this hold.
  • Policy (π) — the state → action mapping an MDP solution produces (which slate to show in a given context).
  • Value function (V, Q) — expected discounted return from a state (V) or a state-action pair (Q) under a policy.
  • Bellman equation — the recursion V*(s) = max_a[R + γ·Σ P·V*] that defines optimal value and underlies every solution method.
  • Value iteration / policy iteration — dynamic-programming solvers used when the model (P, R) is known.
  • Reinforcement learning (Q-learning, policy gradient) — learning a policy from experience when the model is unknown.
  • Exploration vs. exploitation — the trade between the best-known action and an uncertain one that might be better (ε-greedy, UCB, Thompson sampling).
  • POMDP / belief state — partially observable MDP; the true state is inferred from observations via a history summary (a recommender rarely sees real intent, so it's really a POMDP; the EMA vector is a crude belief state).
  • Contextual bandit — a one-step MDP (γ = 0); the frame behind the exploration / ε-greedy slot in §13.3.
  • Discount factor (γ) — weights future vs. immediate reward; γ > 0 is what makes a recommender optimize long-term engagement over next-click.
  • Off-policy evaluation (OPE) — estimating a new policy's value from logged data without deploying it live (IPS, doubly-robust); the practical gate on RL recommenders.
  • SlateQ — a slate-decomposition value-learning approach for recommending sets of items under an RL/MDP formulation.
  • Value of information — the expected cost/benefit of acquiring information (e.g. running research) before deciding; the ROI frame in §14.4.
  • Stochastic dependency graph / PERT — a milestone graph whose edges carry probabilities (gate pass/rework), used to reason about plan time/cost risk (§14.4).

19. Appendix A: Copy-Paste Research Prompts

Ready-to-run prompt bodies. Start a fresh chat, load the Analyst, invoke the workflow, then paste. Replace bracketed parts.

A1 — Technical research (generic template)

bmad-technical-research

Decision: [the one thing to decide].
Options to compare:
1. [option] — [one line]
2. [option] — [one line]
3. [option] — [one line]
Constraints: [latency budget], [runtime/language fit], [existing infra to reuse],
[no-go's].
Output: comparison across [criteria], a recommended default, and a minimal
validation/PoC path.
Depth: [read the options the workflow offers].

A2 — Recommender Pass A (scoring & representation)

bmad-technical-research
Decision: how to represent a user's taste from view history and score candidates.
Options: (1) EMA user vector u←α·e+(1-α)·u + HNSW ANN; (2) recent-k multi-vector /
late-interaction; (3) two-tower learned; (4) hybrid (content ANN + collaborative
rerank). Constraints: O(1)-ish online update (no nightly full recompute),
Java/Spring service, OpenSearch vector store in place, tight p95.
Output: compare on freshness, multi-interest handling, cold-start, infra cost,
online-update cost; recommend a default + migration path.

A3 — Recommender Pass B (failure modes)

bmad-technical-research
Decision: mitigation strategy for recommender failure modes, view-history based.
Cover with a concrete mitigation + pipeline stage: exact duplicates, near-duplicates,
no-diversity, cross-session repetition, popularity bias, cold start, staleness.
Constraints: OpenSearch retrieval, view history queryable (Athena), content has a
cluster/grouping field. Output: a layered defense table, the order to apply layers,
and what each layer misses.

A4 — Recommender Pass C (AWS architecture)

bmad-technical-research
Decision: AWS reference architecture, batch + online paths. Cover: embedding
generation (batch backfill vs near-real-time on ingest), OpenSearch k-NN index +
quantization tradeoff, EMA user-state store (DynamoDB + TTL), serving path (rec
Lambda → ANN retrieve → seen-filter → dedup → MMR rerank → Content API hydrate),
CloudFront+ALB with origin-layer DR, warm/cold path, caching, cost drivers.
Constraints: tight p95, no GPU in request path, existing HNSW index.
Output: component/data-flow description, DR posture, top 3 cost levers.

A5 — Domain research (unfamiliar space)

bmad-domain-research
Domain: [the space]. I need: the terminology practitioners use, how "relevant"/
"good" is judged here vs. elsewhere, regulatory/ethical constraints, and the
evaluation lens I'd otherwise miss.

A6 — Market research (validation)

bmad-market-research
Space: [the category]. Who are the incumbents and adjacent players, the current
trend line, and where are the gaps a fast-follower could exploit?

A7 — Reconcile passes in a panel

bmad-party-mode --mode subagent
Convene: relevance-engineer, latency-hawk, ops-SRE, cost-owner, eval-skeptic.
Inputs (read these files): pass-A-scoring.md, pass-B-failure-modes.md, pass-C-aws.md.
**A8 — Recommender as MDP / RL (should we?)**

bmad-technical-research Decision: should the ranking stage be formulated as an MDP/RL problem (optimizing long-term engagement) or stay a greedy EMA+MMR heuristic (optimizing the next slate)? Frame: S = user vector + session + recent impressions; A = slate; P = stochastic user response; R = engagement/retention; γ = long-term weight. Compare: contextual bandit (one-step), slate-decomposition value learning (SlateQ-style), and the heuristic baseline. Constraints: no live experimentation on every policy (need off-policy evaluation), tight p95, existing OpenSearch/DynamoDB serving. Output: when RL is worth its cost, the off-policy eval plan, serving/complexity implications, and a recommended crawl→walk→run path from the heuristic baseline.


---

## 20. Appendix B: Custom Party Definitions

Author these with `bmad-party-mode, create a new party` (it writes to your overrides
via `bmad-customize`). The exact override-file format is version-specific and the
skill writes it for you — provide the personas and scene below when prompted.
Personas need *teeth*: specific values, first-move behavior, and a blind spot.

**B1 — Reranking / Scoring Decision Panel** (run `--mode subagent`)

- **Rel (Relevance Engineer).** Fights for measured relevance lift (nDCG, MRR,
  recall@k). Opens by asking for the offline metric and the candidate set size.
  Blind spot: discounts operational cost.
- **Pax (Latency Hawk).** Rejects anything that risks the p95 budget or puts a GPU
  in the request path. First question is always "what does this add to tail
  latency?" Blind spot: would ship a fast but mediocre ranker.
- **Sol (Ops/SRE).** Guards index rebuild cost, operational surface, and failure
  modes under region degradation. Blind spot: conservative to a fault.
- **Cass (Cost Owner).** Ranks options by cost-per-query and recompute frequency;
  challenges anything without a cost line. Blind spot: penny-wise on relevance.
- **Vera (Eval Skeptic).** Won't accept any recommendation without a concrete
  experiment that could disprove it. Says "how will we *know*?" in the first thirty
  seconds. Blind spot: can stall a decision that's good enough.

*Scene:* a design review for the recommender's ranking stage; the team must pick one
approach and a validation plan by end of session, and the eval skeptic has veto over
"just ship it."

**B2 — View-History Focus Group (from data)** (run `--mode subagent`)

Hand the room **anonymized user view-history clusters**; it builds representative
personas that react to a proposed recommendation strategy *from their own
behavior*, instead of one model rubber-stamping. Example seeded members:

- **The Specialist.** Reads deep in one narrow area; hates diversity injection that
  pulls them off-topic. Reacts badly to exploration slots.
- **The Generalist.** Grazes across many areas; punished by a single-centroid EMA
  that averages their interests into mush.
- **The Returner.** Comes back weekly; furious when shown items they already read
  (tests your seen-set/impression-discounting).
- **The New Arrival.** Little history; the cold-start canary.

*Scene:* each persona is shown a sample recommendation slate produced by the
candidate strategy and reacts from their own goals; the room surfaces which user
type each design choice helps or hurts.

> **Grounding note (§9.2):** for B2 to be worth anything, the spawned personas must
> actually consume the cluster data you provide — verify tool/file access, or paste
> the cluster summaries directly into the room.

---

## 21. Sources and Recency

Synthesized from the current BMAD-METHOD docs and repository
(docs.bmad-method.org, the `bmad-code-org/BMAD-METHOD` repo, its releases/issues,
and the DeepWiki module reference), plus third-party testing writeups, reflecting
the V6 / BMM state through mid-2026.

Version-sensitive or unverified items, flagged inline where they appear:

- **Research depth option values** — defined in workflow step files, not public
  docs; not verified. Read the options at runtime or ask `bmad-help`.
- **Short vs. `bmm`-qualified command form** — varies by build; `bmad-help` is
  authoritative.
- **The `subagent` grounding regression (§9.2)** and the open request for true
  native-subagent parallelism — both tracked upstream and actively changing;
  re-verify grounding on your build before trusting `subagent` mode for factual,
  code-grounded work.
- **The ~$200 full-cycle token figure (§11)** — one independent measurement, not an
  official number; treat as order-of-magnitude.
- **Custom party override-file format (Appendix B)** — the skill writes it; the
  exact schema is version-specific.

The recommender engineering content in §13 (EMA, MMR, ANN/quantization, the AWS
shape) is stable engineering knowledge, independent of BMAD version.

*Confirm anything load-bearing against your installed version with `bmad-help`.*

Source: Similarity-Calculations · Similarity-Calculations.md · updated 2026-07-23 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

Is binary_clip Using Cosine Similarity? A Ground-Up Explanation

This document answers one question about the mew2026 OpenSearch index:

The binary_clip field is configured with space_type: innerproduct. Is that effectively cosine similarity, or plain dot product? And does using faiss with 16x compression mean the vectors are already normalized?

Short answer, proven with your own data in Section 8: the vectors are not normalized (their lengths are ~9–11, not 1), so this field computes raw dot product, which is not cosine similarity. faiss and the compression level have nothing to do with normalization.

This version assumes no prior background. Every term is defined, the origin of the name is explained (why "L2", where "Euclidean" comes from, what "inner product" means), and every concept has a worked numeric example. If you already know the basics, skip Section 0 and start at Section 1.


Table of Contents

  1. Background: the words and ideas you'll need
  2. The building block: what a vector is
  3. What a "norm" is, and where "L2" comes from
  4. Three ways to measure similarity (with a hand-worked example)
  5. The key insight: normalization makes all three agree
  6. Why innerproduct specifically requires you to normalize
  7. Why faiss and 16x compression do NOT mean "normalized"
  8. How to verify it yourself (the exact steps)
  9. The proof, using your five sampled vectors
  10. Conclusion
  11. How to fix it
  12. Glossary
  13. References

0. Background: the words and ideas you'll need

Before any math, here is the cast of characters. Each is expanded later; this is the map.

Embedding. A machine-learning model reads something complex (an image, a sentence) and outputs a fixed-length list of numbers that captures its "meaning" in a form computers can compare. That list is called an embedding or embedding vector. Similar inputs get similar lists. Your binary_clip field stores one 512-number embedding per news photo.

CLIP. The model that produced these embeddings. CLIP stands for Contrastive Language–Image Pre-training (OpenAI, 2021). It was trained on image–caption pairs so that a picture and its description land at nearby points. A useful consequence: you can search images by text, because both live in the same 512-dimensional space.

Vector / dimension. "Vector" is just the math word for an ordered list of numbers. Each number is one dimension or component. We live in 3 dimensions (left-right, up-down, forward-back), but math places no limit — 512 dimensions simply means "a list of 512 numbers." You can't picture 512-D, but every formula that works in 2-D works identically in 512-D; the examples below use 2-D so you can check them by hand.

Norm. A norm is any rule that measures the "size" or "length" of a vector, written with double bars: ‖v‖. There are several norms; the one everyone means by default is the L2 / Euclidean norm (Section 2).

Similarity metric. The rule for scoring how alike two vectors are. In OpenSearch this is the field's space_type. The three that matter — dot product, cosine, Euclidean — are Section 3.

Normalization. Rescaling a vector so its length becomes exactly 1, keeping its direction. This is the single operation the entire question turns on (Section 4).

faiss. The software library OpenSearch uses under the hood to store vectors and find nearest neighbors quickly. The name stands for Facebook AI Similarity Search. It is a search engine for vectors, not a data-cleaning tool — it does not modify your numbers (Section 6).

HNSW. The specific index structure faiss builds: Hierarchical Navigable Small World graph. It's a shortcut that avoids comparing your query against every stored vector. It affects speed, not the numbers (Section 6).

Quantization / "16x compression". Storing each number in fewer bits to save memory, by rounding it to a coarse set of allowed values. "16x" means 16× smaller than the original. It approximates your numbers; it does not rescale them (Section 6).

With that map in hand, we start from the smallest piece.


1. The building block: what a vector is

CLIP turns an image into an embedding vector: a list of 512 numbers. Picture each vector as an arrow from the origin (the zero point) into space. Two properties describe any arrow:

  • Direction — which way it points. For CLIP, direction carries the meaning: similar images point similar ways.
  • Magnitude / length — how long the arrow is, written ‖v‖. For CLIP this is mostly a model artifact and carries little meaning.

What your actual data looks like (first 8 of the 512 numbers from document MT1IMGOST00034M2TL):

[-0.0992932, 0.3014491, -0.123002, -0.3986786, 0.4534476, -0.470669, 0.1424519, 0.0870281, ...]

Most components are small (±0.1–0.5). But a few, further along, are large (one is −3.33). That mixture is the tell we'll return to.

Why "dimension" here isn't the everyday meaning. In daily life "dimension" means a physical axis. In linear algebra it just means "one slot in the list." 512 dimensions = 512 slots. Nothing is spatial about it; it's bookkeeping. The reason we still use spatial words ("length", "direction", "angle") is that the formulas are the same ones you learned for 2-D and 3-D — they extend to any number of slots unchanged.


2. What a "norm" is, and where "L2" comes from

A norm answers "how big is this vector?" It's a function that takes a vector and returns a single non-negative number: its size. There is more than one sensible way to measure size, so there is a family of norms.

The Lᵖ family (this is where the letters and numbers come from)

The general formula, with a chosen exponent p:

‖v‖ₚ = ( |v₁|ᵖ + |v₂|ᵖ + ... + |vₙ|ᵖ ) ^ (1/p)

Different values of p give different norms:

  • p = 1 → the L1 norm (a.k.a. Manhattan or taxicab distance): just add up the absolute values. It's called "taxicab" because it's how far a taxi drives on a grid of streets — you can't cut diagonally through buildings.
  • p = 2 → the L2 norm (a.k.a. Euclidean norm): square each, sum, take the square root. This is ordinary straight-line length.
  • p = ∞ → the L∞ norm: just the single largest absolute component.

So "L2" literally means "the Lᵖ norm with p = 2." The 2 is the exponent in the formula. And the "L" honors Henri Lebesgue, the French mathematician after whom the Lᵖ spaces of functions are named; the vector norms inherit the letter. (Pedantic note: for finite lists like ours the strictly correct symbol is a lowercase ℓ — "little-ell-2" or ℓ² — while capital L is for continuous functions, but in engineering "L2" is used for both and everyone understands it.)

Why L2 is also called "Euclidean"

Euclid of Alexandria (~300 BCE) wrote the Elements, the foundation of geometry. Straight-line distance in flat space is "Euclidean" in his honor. The L2 formula is nothing more than the Pythagorean theorem (a² + b² = c²) extended to many dimensions:

length of arrow = sqrt( sum of squares of its components )

That's why squaring (exponent 2) and then square-rooting appears: it's Pythagoras. The squaring is what makes it "straight-line" distance rather than grid distance.

One worked example, all three norms

Take v = [3, 4]:

L1  (p=1):  |3| + |4|                 = 7          (add absolute values)
L2  (p=2):  sqrt(3² + 4²) = sqrt(25)  = 5          (Pythagoras — the default "length")
L∞ (max):   max(|3|, |4|)             = 4          (largest component)
import numpy as np
v = np.array([3.0, 4.0])
np.sum(np.abs(v))     # L1  -> 7.0
np.linalg.norm(v)     # L2  -> 5.0   (np.linalg.norm defaults to p=2)
np.max(np.abs(v))     # Linf-> 4.0

The same three on the real 8-number slice above: L1 = 2.076, L2 = 0.8543, L∞ = 0.4707 — different measurements of the same vector.

For the rest of this document, "norm" means L2 (the default everywhere in vector search), and the L2 norm is the single most important calculation here: the test in Section 8 is literally "compute the L2 norm of your real vectors and check whether it equals 1."


3. Three ways to measure similarity (with a hand-worked example)

A similarity metric (OpenSearch's space_type) turns two vectors A and B into one number saying how alike they are. We compute all three on the same pair so the differences are visible.

Example pair: A = [3, 4], B = [4, 0]. (‖A‖ = 5, ‖B‖ = 4.)

3a. Dot product (a.k.a. inner product)

Multiply matching components, sum them:

dot(A, B) = A₁·B₁ + A₂·B₂ = 3·4 + 4·0 = 12

Why two names — "dot product" and "inner product"? "Dot product" comes from the notation A · B (a literal dot). "Inner product" is the more general, abstract term from linear algebra for this whole category of operation; the dot product is the standard inner product on ordinary (Euclidean) space. OpenSearch's innerproduct is exactly this: A · B. The two names mean the same thing here.

The crucial identity — dot product blends direction and length together:

dot(A, B) = ‖A‖ · ‖B‖ · cos(θ)      (θ = the angle between the two arrows)

So a longer vector inflates all its dot products. This is the metric your field uses.

3b. Cosine similarity

Divide the dot product by both lengths — this cancels magnitude and leaves only the angle:

              dot(A, B)        12
cos(A, B) = ───────────── = ─────── = 0.6
             ‖A‖ · ‖B‖       5 · 4

Trig refresher — what "cosine" and "angle" mean here. Cosine is the trigonometric function that, for an angle θ, ranges from +1 at 0° (arrows point the same way), through 0 at 90° (arrows perpendicular — called orthogonal, meaning "unrelated"), to −1 at 180° (arrows point opposite ways). "Angle between two 512-D arrows" sounds exotic, but any two arrows — no matter how many dimensions — always lie in a single flat plane together, and the angle in that plane is an ordinary 0°–180° angle. Cosine similarity is just the cosine of that angle. That's why it's the natural measure of direction alone: it ignores how long the arrows are and reports only how aligned they are.

3c. Euclidean distance (L2 distance)

The straight-line distance between the two arrow tips (smaller = more similar). It reuses the Pythagorean/L2 idea from Section 2, applied to the difference A − B:

d(A, B) = ‖A − B‖ = sqrt((3−4)² + (4−0)²) = sqrt(1 + 16) = sqrt(17) ≈ 4.123

Your field is not using this (space_type is innerproduct, not l2); it's shown for contrast, and to make clear the Euclidean-vs-cosine question is already closed by the config string alone.

All three in code:

import numpy as np
A = np.array([3.0, 4.0]); B = np.array([4.0, 0.0])
A @ B                                            # dot       -> 12.0   ('@' is dot product in numpy)
A @ B / (np.linalg.norm(A) * np.linalg.norm(B))  # cosine    -> 0.6
np.linalg.norm(A - B)                            # euclidean -> 4.1231

Why length "leaking in" actually matters

Make A twice as long (same direction): A2 = [6, 8].

dot(A2, B) = 6·4 + 8·0 = 24        # DOUBLED — purely because A got longer
cos(A2, B) = 24 / (10 · 4) = 0.6   # UNCHANGED — direction didn't change

Under dot product, a document can outrank another just for being "longer," even with identical direction. Under cosine it can't. Hold onto this — it is exactly what's happening in your index.


4. The key insight: normalization makes all three agree

Normalizing rescales a vector to length 1 while preserving direction:

v_normalized = v / ‖v‖

Divide every component by the vector's own L2 length. The result is a unit vector (length 1). Geometrically, all unit vectors sit on the surface of a sphere of radius 1 centered at the origin — the unit sphere — so normalizing is "slide the arrow tip in/out until it touches the unit sphere, without turning it."

Worked example. Normalize A = [3, 4] (‖A‖ = 5):

A_normalized = [3/5, 4/5] = [0.6, 0.8]
check: ‖[0.6, 0.8]‖ = sqrt(0.36 + 0.64) = sqrt(1.0) = 1   ✓
A  = np.array([3.0, 4.0])
An = A / np.linalg.norm(A)   # -> [0.6, 0.8]
np.linalg.norm(An)           # -> 1.0

Now the payoff. Once ‖A‖ = 1 and ‖B‖ = 1:

(i) Dot product becomes cosine — the cosine denominator is 1·1 = 1:

cos(A, B) = dot(A, B) / (‖A‖·‖B‖) = dot(A, B) / 1 = dot(A, B)

Verify: B_normalized = [4/4, 0/4] = [1, 0], so dot(An, Bn) = 0.6·1 + 0.8·0 = 0.6, which equals the cosine we computed earlier. ✓

(ii) Euclidean ranking becomes cosine ranking, via this identity for unit vectors:

d(A,B)² = ‖A‖² + ‖B‖² − 2·dot(A,B) = 1 + 1 − 2·cos(A,B) = 2 − 2·cos(A,B)

Verify: d(An,Bn)² = (0.6−1)² + (0.8−0)² = 0.16 + 0.64 = 0.8, and 2 − 2(0.6) = 0.8. ✓ A bigger cosine always means a smaller distance — identical neighbor order.

The consequence: if your vectors are normalized, it doesn't matter whether the engine runs dot product, cosine, or Euclidean — they all rank identically. That's why teams normalize once at write time, then pick the cheapest metric (dot product) and get cosine behavior for free.

The flip side (your situation): if the vectors are NOT normalized, dot product ≠ cosine, and length distorts the ranking exactly as the doubling demo showed.


5. Why innerproduct specifically requires you to normalize

OpenSearch treats cosinesimil, innerproduct, and l2 as separate space_type values with different internal formulas:

space_typeWhat the engine computesDivides by ‖A‖·‖B‖?_score reported
cosinesimilcosine of the angleYes — built in1 + cos(A,B) (range 0–2)
innerproductraw dot(A,B)Nodot+1 if dot≥0, else 1/(1−dot)
l2squared Euclidean distanceNo (distance-based)1 / (1 + distance²)

The decisive row: cosinesimil bakes the length-division into its own formula. Had your field used cosinesimil, OpenSearch would divide out the magnitudes on every query, giving true cosine regardless of your stored vectors' norms.

But your field is innerproduct, which has no division. It feeds the stored numbers straight into dot(). So innerproduct equals cosine only if you supplied already-normalized vectors. Choosing innerproduct delegates normalization to your ingest pipeline — if ingest doesn't do it, nobody does.

What "score transform" means. A raw dot product can be negative (opposite-pointing vectors). OpenSearch prefers scores to be positive and increasing-with-similarity, so for innerproduct it reports dot + 1 when the dot is ≥ 0 and 1/(1 − dot) when negative. This is monotonic — it never changes the ranking order — but it does mean the _score you see is not a cosine value in [−1, 1]. Don't interpret it as one.

Concrete score example, using two of your real vectors (v1 = MT1IMGOST00034M2TL, v2 = MT1NURPHO000YDLE5G; norms 9.17 and 9.75):

raw dot(v1, v2)                 = 22.2227
cosine(v1, v2)                  =  0.2486     # the "true" semantic similarity (direction only)
innerproduct _score (dot + 1)   = 23.2227     # what your index returns today
cosinesimil _score (1 + cos)    =  1.2486     # what you'd get if normalized / using cosinesimil

ratio raw / cosine = 89.40  ==  ‖v1‖·‖v2‖ = 9.1736 × 9.7454 = 89.40

That 89× ratio is the length inflation ‖v1‖·‖v2‖ from the dot = ‖A‖·‖B‖·cos(θ) identity — pure magnitude injected into the score. After normalizing both vectors, dot(v1n, v2n) = 0.2486, exactly the cosine.

Sample query (the query is identical no matter which metric the field uses — the metric was fixed at index-build time):

GET /mew2026/_search
{
  "size": 5,
  "query": {
    "knn": {
      "binary_clip": {
        "vector": [ /* 512-dim query embedding */ ],
        "k": 5
      }
    }
  }
}

Sample response shape (note the large _score, consistent with un-normalized innerproduct, not a 0–2 cosine score):

{
  "hits": {
    "hits": [
      { "_id": "tag:mewmew.com,2026:newsml_...", "_score": 23.2227, "_source": { } },
      { "_id": "tag:mewmew.com,2026:newsml_...", "_score": 19.8410, "_source": { } }
    ]
  }
}

6. Why faiss and 16x compression do NOT mean "normalized"

This is the specific misconception, so each config element is dissected on its own.

"method": {
  "engine": "faiss",
  "space_type": "innerproduct",
  "name": "hnsw",
  "parameters": {}
},
"mode": "on_disk",
"compression_level": "16x"

engine: faiss — a search library, not a normalizer

faiss (Facebook AI Similarity Search) builds the search structure and computes distances. It computes exactly the metric space_type names (dot() here) on exactly the numbers it's given. faiss performs no automatic normalization — in its own API you must call faiss.normalize_L2(x) on your data yourself if you want cosine, and OpenSearch does not call that for you on an innerproduct field. So faiss tells you how search runs, not whether the data was normalized.

# faiss "cosine" is literally: normalize FIRST, then use inner product.
import faiss, numpy as np
x = np.random.rand(1000, 512).astype('float32')
faiss.normalize_L2(x)           # <-- YOU do this explicitly; nothing is implicit
index = faiss.IndexFlatIP(512)  # IP = Inner Product
index.add(x)                    # only now does IP behave like cosine

name: hnsw — the index shape, unrelated to magnitude

HNSW (Hierarchical Navigable Small World) is a graph that lets a query reach its nearest neighbors in a few hops instead of comparing against all 10,000+ vectors. ("Small world" is the same idea as "six degrees of separation" — most points are only a few hops apart.) It changes which candidates are compared and how fast — it never rescales a vector.

compression_level: 16x — lossy storage, not rescaling

First, two words of background:

What a "bit" and a "float" are. A bit is a single 0/1 digit; n bits can represent 2ⁿ distinct values (2 bits → 4 values, 8 bits → 256). Your original numbers are float32 — 32-bit floating-point numbers (the standard IEEE 754 format), giving very fine precision. "Compression" here means storing each number in fewer bits by rounding it to a small menu of allowed values. That rounding is called quantization.

"16x" means 16× smaller than float32, i.e. 32 ÷ 16 = 2 bits per number — so each of the 512 numbers is rounded to one of just 2² = 4 allowed levels. Quantization approximates the numbers you already have; it does not rescale the vector to length 1. A rounded copy of a length-9 vector is still ~length 9.

Compression-level → bits, per the OpenSearch docs:

compressionbits/dimlevels (2ⁿ)note
32x12true binary quantization
16x24your field
8x416
4x8 (byte)256
2x16 (fp16)65 536

Why un-normalized data makes compression worse (toy 2-bit example). With only 4 levels spread across the data's range, if one outlier sits at 3.33 the levels must stretch to cover it, and the ~500 small values collapse together:

value  +0.030  -> nearest of 4 levels = +0.1667   (error 0.137)   # informative values get crushed
value  +0.210  -> nearest of 4 levels = +0.1667   (error 0.043)
value  -0.440  -> nearest of 4 levels = -0.5000   (error 0.060)
value  +3.330  -> clipped to           +0.5000   (error huge)     # the outlier wrecks the scale

Normalizing first shrinks those outliers and lets the 4 levels resolve the informative dimensions — so normalization would improve compression, not replace it.

Aside (on_disk random rotation). on_disk applies a random rotation before quantizing, to spread information more evenly across dimensions. A rotation preserves length (it spins the arrow without stretching it), so it also does not normalize.

mode: on_disk — rescoring uses the same un-normalized vectors

on_disk runs two phases: a fast approximate pass over the compressed vectors, then a rescore using the full-precision vectors from disk (default oversample_factor 2.0). Those full-precision vectors are the same un-normalized numbers, so rescoring makes the score more faithful to the raw dot product — moving you further from cosine, not toward it.

Bottom line: none of faiss, hnsw, 16x, or on_disk inspects or changes vector length. The only thing that makes innerproduct behave as cosine is an explicit unit-normalize in your ingest code before indexing. Whether that ran is a question about the numbers in _source — which we check next.


7. How to verify it yourself (the exact steps)

Step 1 — confirm the metric (rules out Euclidean):

GET /mew2026/_mapping/field/binary_clip

Read ...binary_clip.method.space_type. Yours = innerproduct → not l2, so Euclidean is out; only the normalization question remains.

Step 2 — pull a few stored vectors:

GET /mew2026/_search
{
  "size": 5,
  "_source": ["binary_clip"],
  "query": { "exists": { "field": "binary_clip" } }
}

Step 3 — compute each vector's L2 norm. Normalized ⇒ every norm ≈ 1.0000. Not normalized ⇒ the norms are something else and vary:

import numpy as np
for h in resp["hits"]["hits"]:
    v = np.asarray(h["_source"]["binary_clip"], dtype=np.float64)
    print(h["_id"][-18:], "norm =", round(float(np.linalg.norm(v)), 4))

Step 4 (optional, conclusive) — scale-invariance check. Query with a stored vector, then again with a ×3 copy of it. True cosine is scale-invariant (same order); raw dot product changes the scores/order. This is the live-query analog of the doubling demo in Section 3.

Step 5 (fallback if binary_clip is excluded from _source) — inspect the ingest code for a normalize step: x / np.linalg.norm(x), sklearn.preprocessing.normalize, or torch.nn.functional.normalize(emb, dim=-1). Hugging Face CLIPModel returns un-normalized image_embeds; the standard pattern is emb = emb / emb.norm(p=2, dim=-1, keepdim=True) right after the forward pass. Present ⇒ cosine-equivalent; absent ⇒ raw dot product.


8. The proof, using your five sampled vectors

You returned five documents from mew2026 with full binary_clip arrays in _source. Applying Step 3:

Results (exact, computed from your data)

Document (_id tail)DimL2 normLargest |component|RMS per component
MT1IMGOST00034M2TL5129.17363.32770.4054
MT1NURPHO000YDLE5G5129.74545.32050.4307
MT1NURPHO0003GDRTT51210.75796.17410.4754
RC2VZKAGA2RW51210.46055.03180.4623
MT1SIPA000UKMLE25129.64903.61920.4264

Summary: mean norm 9.96, range 9.17 → 10.76 (spread ~1.58 across just five docs).

What "RMS per component" is and why it's here. RMS = root-mean-square = sqrt(average of the squares) = ‖v‖ / sqrt(512). It's the "typical size" of a single number in the vector. For a correctly normalized 512-D vector the total length is 1, so each component is tiny: RMS ≈ 1/√512 ≈ 0.0442. Your RMS is ~0.43 — about 10× too large, matching the norm being ~10× too large. Two independent framings, same conclusion.

What the numbers prove

1. Norms are ~9–11, not ~1 → never normalized. A normalize step would force every norm to exactly 1.0000. Instead they sit near 10.

2. You don't even need the exact norm. A unit vector cannot have any component with magnitude > 1: if a component were 3.3, then ‖v‖ ≥ 3.3 > 1 by the norm formula (the full length is at least as big as any one leg). Every sampled vector has a component past ±1 — up to 6.17. That single fact rules out normalization.

3. Norms vary doc-to-doc (9.17 vs 10.76) → the dot-product fingerprint. From dot = ‖A‖·‖B‖·cos(θ), differing lengths mean the score is scaled by each doc's own length, not by meaning alone. Under true cosine every vector contributes length 1 and this variation disappears.

4. The CLIP "rogue dimension." The largest-magnitude component sits at the same index (~92) and is strongly negative across documents: −3.33, −5.32, −6.17, −5.03, −3.62. Concretely, for the v1·v2 pair:

dimension 92:  v1[92] = -3.3277,  v2[92] = -5.3205
   product term = (-3.3277) × (-5.3205) = 17.7050
   median product term across all 512 dims = 0.0363
   -> this ONE dimension contributes ~488× a typical dimension

The raw dot product between these two vectors is 22.22 — and a single dimension supplies 17.71 of it (≈80%). So similarity is decided mostly by one outlier dimension, not the full 512-D semantic signal. This is the well-documented CLIP outlier-dimension effect, and L2 normalization is exactly the step that tames it.


9. Conclusion

  • Metric in use: raw inner (dot) productspace_type: innerproduct does no length-division.
  • Euclidean? No — the space_type would read l2.
  • Cosine in effect? No. innerproduct equals cosine only on unit vectors; your sampled vectors have norms ~9–11.
  • Do faiss / 16x / on_disk imply normalization? No. They govern the search library, index structure, memory compression, and rescoring — none touches vector length. Only an ingest-time normalize would, and the norms prove it didn't run.

Net effect: the field ranks by direction and magnitude mixed together, with a single CLIP outlier dimension dominating (~80% of the score in the sampled pair) — not the pure angular (cosine) similarity you normally want from CLIP.


10. How to fix it

Option A — normalize at ingest (recommended). Unit-normalize every embedding before writing, and normalize the query vector identically. Then innerproduct becomes true cosine, outlier dims stop dominating, and quantization improves.

import numpy as np

def to_unit(vec):
    v = np.asarray(vec, dtype=np.float32)
    n = np.linalg.norm(v)
    if n == 0:
        raise ValueError("zero vector cannot be normalized")
    return v / n

emb = to_unit(clip_image_embedding)              # ||emb|| == 1 now
assert abs(np.linalg.norm(emb) - 1.0) < 1e-3     # guardrail so it can't silently regress
# ...index emb into binary_clip...

Option B — switch the field to cosinesimil. OpenSearch then does the division itself and you get cosine regardless of input norms:

PUT /mew2026_v2
{
  "settings": { "index": { "knn": true } },
  "mappings": {
    "properties": {
      "binary_clip": {
        "type": "knn_vector",
        "dimension": 512,
        "space_type": "cosinesimil",
        "mode": "on_disk",
        "compression_level": "16x",
        "method": { "name": "hnsw", "engine": "faiss" }
      }
    }
  }
}

Either way it's a reindex, because the metric is baked into the HNSW graph at build time — you can't change space_type on a live field. A typical flow: create the new index (with normalized ingest, or cosinesimil), reindex/re-embed into it, then swap an alias so callers don't change their queries:

POST /_aliases
{
  "actions": [
    { "remove": { "index": "mew2026",    "alias": "clip_search" } },
    { "add":    { "index": "mew2026_v2", "alias": "clip_search" } }
  ]
}

Sanity check after fixing — re-run Section 7 Step 3; every norm should now read ~1.0000, and innerproduct _score values should fall into the 0–2 band (dot ∈ [−1, 1] → score = dot + 1).

Scope note: this was a 5-document sample. It's conclusive that normalization is absent (norms ~10; components > 1 are impossible for unit vectors), but for full confidence run the Step-3 norm check over a larger random sample — expect the same result unless different code paths write to this field differently.


11. Glossary

  • Bit — a single binary digit (0 or 1). n bits encode 2ⁿ distinct values.
  • CLIP — Contrastive Language–Image Pre-training; the OpenAI model that produced these image embeddings. Trained so images and their text captions land near each other.
  • Component / dimension — one number (one slot) in a vector. Your vectors have 512.
  • Cosine similarity — the cosine of the angle between two vectors; measures direction only, ignoring length. Range −1 to +1.
  • Dot product / inner product — sum of products of matching components (A·B). Blends direction and length. OpenSearch calls it innerproduct.
  • Embedding — a fixed-length list of numbers a model outputs to represent an input's meaning.
  • Euclidean — pertaining to ordinary flat-space geometry (after Euclid, ~300 BCE); the L2 norm/distance is "Euclidean" because it's the straight-line Pythagorean length.
  • faiss — Facebook AI Similarity Search; the library OpenSearch uses to store and search vectors. Does not modify your numbers.
  • float32 — a 32-bit floating-point number (IEEE 754); the high-precision format the original embeddings use before compression.
  • HNSW — Hierarchical Navigable Small World; the graph index that finds nearest neighbors in a few hops instead of scanning everything.
  • L1 / L2 / L∞ norm — members of the Lᵖ norm family (exponent p = 1, 2, ∞). L1 = sum of absolute values (Manhattan); L2 = square-root of sum of squares (Euclidean, the default "length"); L∞ = largest absolute component. "L" honors Henri Lebesgue.
  • Magnitude / length / norm — how "big" a vector is; ‖v‖. Unqualified, it means the L2 norm.
  • Normalization (unit-normalize) — rescaling a vector to length 1 by dividing by its own norm; keeps direction, removes length.
  • Orthogonal — at a 90° angle; cosine 0; "unrelated" directions.
  • Quantization — rounding numbers to a small set of allowed values to store them in fewer bits (lossy compression).
  • Rescore (on_disk) — a second pass that recomputes scores using full-precision vectors after a fast approximate first pass.
  • space_type — the OpenSearch setting naming the similarity metric for a vector field (cosinesimil, innerproduct, or l2).
  • Unit sphere / unit vector — the set of all length-1 vectors / a vector of length 1.

12. References

  • OpenSearch — Spaces (space_type definitions and score formulas for cosinesimil, innerproduct, l2): https://docs.opensearch.org/latest/mappings/supported-field-types/knn-spaces/
  • OpenSearch — k-NN vector field type: https://docs.opensearch.org/latest/mappings/supported-field-types/knn-vector/
  • OpenSearch — Disk-based vector search (on_disk mode, two-phase rescoring, oversample_factor default 2.0): https://docs.opensearch.org/latest/vector-search/optimizing-storage/disk-based-vector-search/
  • OpenSearch — Memory-optimized vectors (compression_level, rescoring): https://docs.opensearch.org/latest/mappings/supported-field-types/knn-memory-optimized/
  • OpenSearch — Binary quantization (compression-level → bits/dimension: 32x=1-bit, 16x=2-bit, 8x=4-bit): https://docs.opensearch.org/3.0/vector-search/optimizing-storage/binary-quantization/
  • faiss wiki — inner product vs L2, and normalize_L2 for cosine: https://github.com/facebookresearch/faiss/wiki
  • CLIP — Radford et al., Learning Transferable Visual Models From Natural Language Supervision (2021); embeddings are compared after L2 normalization: https://arxiv.org/abs/2103.00020
  • Lᵖ spaces / Lebesgue (background on the norm family and its naming): https://en.wikipedia.org/wiki/Lp_space

(All numeric results in this document were computed directly from the five binary_clip vectors you sampled from mew2026.)

Source: vector_plot · vector_plot.md · updated 2026-07-10 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

Build a single-file HTML tool (vanilla JS + Chart.js from CDN, no build step) called vector-profile-viewer.html that visualizes recommendation embeddings against a base vector.

image

Input

The tool accepts JSON via (a) a file picker and (b) a paste-in textarea. Schema:

{ "metadata": { "dim": 512, "metric": "cosine", // "cosine" | "l2" | "dot" "model": "optional-model-name", "normalized": true // whether vectors are already L2-normalized }, "base": { "label": "User profile (avg of history)", "vector": [0.012, -0.034, ...] // length must equal metadata.dim }, "items": [ { "id": "item-8841", "label": "Item title", "score": 0.9612, // similarity returned by the ANN engine "vector": [0.011, -0.029, ...] } // ... up to ~50 items ] }

Validate on load: all vectors match metadata.dim, no NaNs, items non-empty. Show clear error messages, never a blank page.

Processing

  1. If metadata.normalized is false OR any vector norm deviates from 1.0 by more than 1%, L2-normalize all vectors before plotting and show a notice ("vectors were normalized for display").
  2. Recompute cosine similarity of every item to the base locally; if it differs from the provided score by > 0.01, flag that item in the UI (helps catch metric mismatches with the ANN engine).
  3. Sort dimensions by the base vector's value (argsort). All plots use this shared column/x order.
  4. Rank items by recomputed similarity, descending.

Views (tabs or stacked sections)

View 1 — Sorted line profile (default)

  • X axis: dimension rank 1..dim (sorted by base value), label it as such.
  • Dark 2.5px line: base vector.
  • Gray band (fill, ~18% opacity): per-dimension min/max envelope across all items.
  • Highlighted lines: closest item (solid green #1baf7a), median item (dashed blue #2a78d6), farthest item (dotted red #e34948). 1.75px.
  • Dropdown to swap any highlighted slot for a specific item by label.
  • Toggle: raw values vs rolling-mean smoothing (window 5), default raw, state shown on the axis label.
  • Y axis: auto-fit to data with 10% padding; if normalized, expect ~±0.15. If any |component| > 5x the median absolute value, list those dims as "outlier dimensions" below the chart instead of letting them flatten it, with a toggle to clip them from the y-range.
  • Custom HTML legend above the chart (small squares + labels + sim scores), not Chart.js's default legend. Dash patterns must appear in the legend.

View 2 — Delta heatmap

  • Canvas, one row per item (ranked by similarity, best on top), one 1px column per dimension (same sorted order).
  • Cell value = item[d] − base[d]; diverging colormap blue↔gray↔red centered at 0, symmetric scale from the 99th percentile of |delta|.
  • Row labels: "#rank · label · sim". Hover tooltip: dim index (original, pre-sort), base value, item value, delta.

View 3 — Summary

  • Ranked horizontal lollipop chart of similarity to base.
  • Stat tiles: n items, mean sim, min sim, mean pairwise sim among items (redundancy indicator).

General

  • Everything client-side, no network calls except the Chart.js CDN.
  • Include a "Load demo data" button that generates a synthetic base + 20 items with decaying similarity so the tool works before real data is wired in.
  • Include an "Export PNG" button per chart and "Copy processed JSON" (with recomputed sims and sort order) for downstream use.
  • Clean, minimal styling; must be readable in light and dark OS themes.

Source: ios_sensors · ios_sensors.md · updated 2026-06-22 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

iPhone Activity Detection: Walking vs Driving

A Deep-Dive Into Sensor Fusion, CoreMotion, CoreLocation, and the Physics Behind It


Document scope: This reference covers the full stack — from the silicon-level motion coprocessor through iOS frameworks to the application-level fusion classifier — explaining not just what each API does but why it works and when it fails. Code examples are in Swift (iOS 16+). Framework references map to Apple's developer documentation as of iOS 17 / Xcode 15.


Table of Contents

  1. The Problem: Why GPS Alone Is Not Enough
  2. The Hardware Layer: Sensors Inside the iPhone
  3. The Framework Layer: Apple's CoreMotion & CoreLocation
  4. Signal Characteristics: What Each Activity Looks Like
  5. The Physics and Mathematics Behind Each Signal
  6. Full Implementation Walkthrough
  7. Scenario Walkthroughs
  8. Edge Cases and Known Failure Modes
  9. Real-World Use Cases and Applications
  10. Battery and Performance Considerations
  11. Improving Accuracy: Advanced Techniques
  12. Privacy, Permissions, and App Store Compliance
  13. Testing Strategy
  14. Broader Perspectives: How Other Platforms Approach This
  15. References and Further Reading

1. The Problem: Why GPS Alone Is Not Enough

At first glance, distinguishing walking from driving seems trivial: check the speed. A person walks at roughly 1.4 m/s; a car moves at 14 m/s or more. Just threshold on GPS velocity — done.

In practice this fails in at least five common situations:

Traffic jams. A car crawling at 0.5 m/s through gridlock produces a GPS reading indistinguishable from brisk walking. GPS alone would classify the driver as a pedestrian.

GPS lag and multipath. In urban canyons — surrounded by tall buildings — GPS signals bounce off surfaces before reaching the receiver. This causes "multipath error" where the reported position jumps, making computed speed unreliable or wildly incorrect. A pedestrian walking steadily might briefly read 12 m/s due to a bad fix.

GPS unavailability. Tunnels, underground car parks, dense indoor environments, and some rural areas with weak satellite coverage all produce invalid speed readings (CLLocation.speed == -1). The app must degrade gracefully and rely on other signals.

Speed overlap zones. A slow cyclist (3–5 m/s) overlaps with a runner (2.5–5 m/s). An electric scooter overlaps with a fast cyclist. Speed alone cannot separate these.

Cold starts. A GPS receiver that has been off for minutes needs 30–90 seconds to acquire satellites and produce an accurate fix. During that window, the initial speed readings can be zero or garbage.

The solution is sensor fusion: combining multiple independent signal sources so that each compensates for the others' weaknesses. GPS is unreliable in tunnels; the accelerometer still works underground. The coprocessor is unreliable at 0.5 m/s; GPS is accurate above 6 m/s. Together they cover the failure modes that no single sensor handles alone.


2. The Hardware Layer: Sensors Inside the iPhone

2.1 Accelerometer

The accelerometer is a MEMS (Micro-Electro-Mechanical Systems) device — a microscopic silicon proof mass suspended by springs inside the chip. When the device accelerates, the proof mass deflects relative to the housing. That deflection is measured capacitively (by how the gap between two conducting plates changes) and converted to a voltage proportional to acceleration in each axis.

Modern iPhones use a three-axis accelerometer, producing independent readings along X (left–right), Y (up–down), and Z (front–back) when the phone is held upright. The raw output is in units of g (standard gravity, 9.81 m/s²).

Important note on gravity: Even when the phone is completely stationary, the accelerometer reads approximately 1 g on whichever axis points toward the ground, because it is measuring the electromagnetic contact force that counteracts gravity, not true inertial acceleration. This is why computing the magnitude of the acceleration vector and subtracting 1 g (or comparing variance rather than absolute values) is important for motion analysis.

Sampling rates available through CoreMotion:

RateTypical use
10 HzBackground step detection, coarse activity
50 HzReal-time activity classification (our use case)
100 HzGesture recognition, sports tracking
200–400 Hz (device-dependent)Vibration analysis, fall detection

The accelerometer consumes roughly 0.5–1 mA at 50 Hz — modest compared to GPS (~20–30 mA) but non-trivial for background apps.

2.2 Gyroscope

The gyroscope measures angular velocity (rotational rate) in degrees/second or radians/second around each axis. It uses a vibrating MEMS structure; Coriolis forces cause the vibration axis to shift during rotation, which is measured capacitively.

In activity detection, the gyroscope is less directly useful than the accelerometer, but it contributes to:

  • Device orientation tracking — knowing whether the phone is in a pocket, on a table, or in a hand changes how you interpret accelerometer data.
  • Reducing false positives from hand gestures — shaking the phone while sitting still produces accelerometer variance that could be mistaken for walking. The gyroscope signature of a hand shake (rapid, uncorrelated rotation) differs from a walking signature (periodic linear oscillation with minimal rotation).
  • CMDeviceMotion — the CMDeviceMotion API fuses accelerometer + gyroscope + magnetometer using a complementary filter to produce stable attitude (pitch, roll, yaw) estimates. This is not used in our basic implementation but is available.

2.3 Barometer

The barometer measures atmospheric pressure, which decreases at approximately 12 Pa per metre of altitude gain. This makes it useful for floor-level detection in multi-story buildings and staircase recognition.

In walking vs driving detection, the barometer is a secondary signal: a person climbing stairs shows accelerometer patterns similar to walking but with steadily decreasing pressure; a car going up a parking structure ramp shows driving-speed GPS with a slow pressure drop.

2.4 GPS / GNSS Receiver

Modern iPhones support multiple GNSS (Global Navigation Satellite System) constellations simultaneously:

  • GPS — US Department of Defense, ~31 satellites
  • GLONASS — Russian system, ~24 satellites
  • Galileo — European Union, ~28 satellites
  • BeiDou — Chinese system, ~35 satellites
  • QZSS — Japanese regional system (useful in East Asia)

Supporting multiple constellations means more visible satellites at any given location, which dramatically improves fix accuracy and reduces time-to-first-fix. In open sky, modern iPhones achieve horizontal accuracy of 3–5 metres.

The iOS CLLocation object exposes:

location.speed             // m/s, or -1 if invalid
location.speedAccuracy     // m/s 1-sigma error; negative means invalid
location.horizontalAccuracy // metres, ~68% confidence radius
location.coordinate        // CLLocationCoordinate2D
location.altitude          // metres above sea level (WGS-84)
location.timestamp         // Date of the reading

The speedAccuracy field is critical and often ignored. A speed reading of 1.5 m/s with speedAccuracy of 0.3 m/s is quite reliable; the same speed reading with speedAccuracy of 4.0 m/s is useless noise. The ActivityDetector implementation checks speedAccuracy >= 0 before trusting the speed.

2.5 The Motion Coprocessor (M-series chip)

Starting with the iPhone 5s (A7 chip with M7 coprocessor), Apple added a dedicated low-power motion coprocessor that runs independently of the main CPU. Even when the iPhone is in deep sleep — screen off, app in background — the M-series chip continues collecting and classifying motion data.

The coprocessor:

  • Samples the accelerometer at low rates continuously
  • Runs a built-in activity classifier (believed to use a Hidden Markov Model — see Section 5.4)
  • Stores up to 7 days of historical activity data in hardware, accessible retrospectively via CMMotionActivityManager.queryActivityStarting(from:to:to:withHandler:)
  • Consumes approximately 0.1 mA — orders of magnitude less than keeping the main CPU awake

This is why CMMotionActivityManager is the recommended first-line API for activity detection. You get high-quality classification essentially for free, power-wise.


3. The Framework Layer: Apple's CoreMotion & CoreLocation

3.1 CMMotionActivityManager — the coprocessor shortcut

CMMotionActivityManager is the highest-level API available. It surfaces the coprocessor's built-in activity classifier as a stream of CMMotionActivity objects.

Key properties of CMMotionActivity:

activity.walking     // Bool — user is likely walking
activity.running     // Bool — user is likely running
activity.cycling     // Bool — user is likely cycling
activity.automotive  // Bool — user is likely in a vehicle
activity.stationary  // Bool — user is likely not moving
activity.confidence  // CMMotionActivityConfidence: .low | .medium | .high
activity.startDate   // Date when this activity started

Note that multiple booleans can be true simultaneously. This happens during transitions — e.g., stepping out of a car, automotive and walking are both true briefly. The confidence property reflects how certain the coprocessor is.

Checking availability:

guard CMMotionActivityManager.isActivityAvailable() else {
    // Older devices (pre-iPhone 5s) or non-supported hardware
    return
}

Starting real-time updates:

let manager = CMMotionActivityManager()

manager.startActivityUpdates(to: .main) { activity in
    guard let activity = activity else { return }
    
    // Priority order matters — automotive takes precedence
    if activity.automotive {
        print("Driving — confidence: \(activity.confidence)")
    } else if activity.running {
        print("Running")
    } else if activity.walking {
        print("Walking")
    } else if activity.stationary {
        print("Stationary")
    }
}

Querying historical data:

let yesterday = Date().addingTimeInterval(-86400)
let now = Date()

manager.queryActivityStarting(from: yesterday, to: now, to: .main) { activities, error in
    guard let activities = activities, error == nil else { return }
    
    let drivingTime = activities
        .filter { $0.automotive && $0.confidence != .low }
        .map { $0.startDate }
        // compute durations between consecutive events...
    
    print("Activities in past 24h: \(activities.count)")
}

Required plist key:

<key>NSMotionUsageDescription</key>
<string>Used to detect whether you are walking or driving to adapt app behaviour.</string>

3.2 CMPedometer — step detection and cadence

CMPedometer provides higher-level walking metrics derived from the accelerometer via the motion coprocessor:

  • numberOfSteps — cumulative steps since the start date
  • currentPace — current pace in seconds per metre (⚠ not per step)
  • currentCadence — steps per second (iOS 9+)
  • distance — estimated walking distance in metres
  • floorsAscended / floorsDescended — floors climbed/descended (requires barometer)

Current cadence vs. current pace: A subtle but important distinction. currentCadence is directly steps/second. currentPace is seconds/metre — it represents how long it takes to cover a metre of ground, combining both step frequency and stride length. For activity classification, cadence is more directly useful because it doesn't depend on stride length estimation.

let pedometer = CMPedometer()

guard CMPedometer.isStepCountingAvailable() else { return }

pedometer.startUpdates(from: Date()) { data, error in
    guard let data = data, error == nil else { return }
    
    // currentCadence is steps/second directly
    if let cadence = data.currentCadence {
        let stepsPerSecond = cadence.doubleValue
        print("Cadence: \(stepsPerSecond) steps/sec")
    }
    
    // currentPace is sec/metre — invert for steps/sec if no cadence available
    if let pace = data.currentPace {
        let secondsPerStep = pace.doubleValue
        let stepsPerSecond = 1.0 / secondsPerStep
        print("Step freq (from pace): \(stepsPerSecond) steps/sec")
    }
}

Why cadence is a powerful discriminator:

Human gait has a remarkably consistent cadence range. Across all ages and heights:

  • Strolling: 0.8–1.2 steps/sec
  • Normal walking: 1.4–1.8 steps/sec
  • Brisk walking: 1.8–2.5 steps/sec
  • Running: 2.5–4.0 steps/sec
  • Sprint: 4.0–5.0 steps/sec

A person sitting in a car, even on a bumpy road, registers essentially 0 steps/sec. This makes near-zero cadence a strong vote for driving or stationary.

Checking device support before using:

CMPedometer.isStepCountingAvailable()  // accelerometer-based step count
CMPedometer.isCadenceAvailable()       // iOS 9+, requires M7+
CMPedometer.isDistanceAvailable()      // M7+ with motion coprocessor
CMPedometer.isFloorCountingAvailable() // requires barometer

3.3 CMMotionManager — raw accelerometer data

CMMotionManager provides direct access to raw accelerometer (and gyroscope, magnetometer, and device motion) streams. Unlike CMMotionActivityManager, this runs on the main application processor — it stops when the app is backgrounded unless Background Modes are enabled.

Setting up 50 Hz accelerometer updates:

let motionManager = CMMotionManager()

guard motionManager.isAccelerometerAvailable else { return }

// 50 Hz = 0.02 second interval
motionManager.accelerometerUpdateInterval = 1.0 / 50.0

motionManager.startAccelerometerUpdates(to: .main) { data, error in
    guard let data = data, error == nil else { return }
    
    // data.acceleration is in g (1 g ≈ 9.81 m/s²)
    let x = data.acceleration.x
    let y = data.acceleration.y
    let z = data.acceleration.z
    
    // Magnitude of the acceleration vector
    let magnitude = sqrt(x*x + y*y + z*z)
    
    // At rest: magnitude ≈ 1.0 (gravity vector)
    // Walking: magnitude oscillates ~0.8–1.2 with each step
    // Driving on smooth road: magnitude ≈ 1.0 with small perturbations
    // Pothole or speed bump: magnitude spikes to 1.5–2.5 briefly
}

CMAcceleration axes (device held upright, portrait):

     +Y (up)
      │
      │
──────┼────── +X (right)
      │
      │
    (screen faces +Z)

Since phone orientation varies (pocket, bag, car mount), axis-specific analysis is fragile. The magnitude sqrt(x² + y² + z²) is orientation-invariant and is what we use for variance computation.

Stopping updates properly:

motionManager.stopAccelerometerUpdates()
// Also available:
motionManager.stopGyroUpdates()
motionManager.stopDeviceMotionUpdates()

3.4 CLLocationManager — GPS speed and heading

CLLocationManager is the gateway to all location and heading data. For activity detection, the key fields are speed and speedAccuracy.

Accuracy modes and their trade-offs:

// Best accuracy — full GPS, heavy battery use
locationManager.desiredAccuracy = kCLLocationAccuracyBest

// ~10m accuracy, moderate battery
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters

// ~100m accuracy — uses cell towers + WiFi, very battery-efficient
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

For activity detection, kCLLocationAccuracyBest is recommended because we need the Doppler-derived velocity measurement, which requires a full GNSS fix. Cell tower triangulation does not produce a reliable speed value.

Filtering out bad speed readings:

func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }
    
    // Negative speedAccuracy means the speed reading is invalid
    guard location.speedAccuracy >= 0 else { return }
    
    // Optional: filter out extremely noisy readings
    guard location.speedAccuracy < 2.0 else { return } // m/s error too high
    
    // speed can be -1 if no valid speed measurement; clamp to 0
    let speed = max(0, location.speed)
    
    print("Speed: \(speed) m/s  ±\(location.speedAccuracy) m/s")
}

The distanceFilter property controls how many metres the device must move before a location update fires. Setting distanceFilter = 2 (metres) for walking detection is a good balance — it prevents excessive callbacks when stationary while still catching slow pedestrian movement.


4. Signal Characteristics: What Each Activity Looks Like

4.1 GPS Speed Profile

ActivityTypical speed rangeNotes
Stationary0–0.3 m/sGPS noise floor; readings oscillate even when standing still
Slow walk0.5–1.2 m/sOlder adults, browsing on a phone
Normal walk1.2–1.8 m/sAverage adult pace (~4.5 km/h)
Brisk walk1.8–2.2 m/sCommuter pace
Running (slow jog)2.2–4.0 m/s8–14 min/km
Running (fast)4.0–7.0 m/sSub-8 min/km
Cycling (slow)3.0–5.0 m/sUrban cycling
Cycling (fast)5.0–12.0 m/sRoad cycling
Traffic jam driving0–2.0 m/sOverlap with walking range
Urban driving6.0–14.0 m/s22–50 km/h
Motorway driving22–40+ m/s80–145+ km/h

The overlap between walking, slow cycling, and traffic-jam driving at 0–2 m/s is the core challenge GPS speed alone cannot resolve.

4.2 Step Frequency Profile

ActivitySteps/secondNotes
Stationary0Occasional fidget may produce 0.1–0.3 briefly
Strolling0.8–1.2Window shopping, corridors
Normal walk1.4–1.8Most adults
Brisk walk1.8–2.5Exercise walking
Running2.5–4.0Increases with speed
Sprint4.0–5.0Elite sprinters up to 5.5
Cycling0No footsteps (pedalling doesn't register)
Driving0No footsteps

Step frequency is the cleanest discriminator between walking/running and everything else. Zero cadence rules out walking or running entirely.

4.3 Accelerometer Variance Profile

Variance measures how much the acceleration magnitude fluctuates around its mean over a 1-second window.

ActivityTypical varianceExplanation
Stationary on table<0.001Essentially no motion
Held stationary0.002–0.010Micro hand tremors
Driving (smooth motorway)0.005–0.020Engine vibration, smooth road
Driving (city with potholes)0.020–0.100Road surface irregularities
Walking (in pocket)0.020–0.080Hip-transmitted step bounce
Walking (in hand)0.050–0.200Arm swing adds to hip movement
Running0.100–0.400High-impact footstrikes
Cycling0.010–0.050Smooth cadence, less up-down

Key insight: Driving on a smooth motorway has similar variance to a phone held in a steady hand, which is lower than walking variance. However, driving over potholes can spike variance to values indistinguishable from walking. This is why variance alone is an unreliable discriminator — it's best used as supporting evidence when GPS or pedometer data is ambiguous.

4.4 Summary Signal Table

A quick reference for the fusion logic:

SignalWalkingDrivingRunningCyclingStationary
GPS speed (m/s)0.5–2.2>6 (usually >10)2.2–73–120–0.3
Cadence (steps/s)1.2–2.5~02.5–5~0~0
Accel variance0.02–0.20.005–0.050.1–0.40.01–0.05<0.01
CMMotionActivity.walking.automotive.running.cycling.stationary

5. The Physics and Mathematics Behind Each Signal

5.1 How GNSS computes speed (Doppler vs position-differencing)

There are two methods a GNSS receiver can use to compute speed:

Position-differencing (less accurate):

speed ≈ Δposition / Δtime

You compute two positions 1 second apart and divide the distance by the elapsed time. This is simple but inherits all the noise of position estimation — if either position has a 5-metre error, the computed speed can be wildly wrong at pedestrian speeds.

Doppler measurement (far more accurate):

When a satellite and the receiver are in relative motion, the frequency of the satellite's carrier signal shifts by the Doppler effect:

Δf = (v_rel / c) × f_carrier

Where v_rel is the relative radial velocity, c is the speed of light (~3×10⁸ m/s), and f_carrier is the GPS L1 carrier frequency (1575.42 MHz).

Modern GNSS receivers, including those in iPhones, measure this Doppler shift directly from the carrier phase tracking loop. Because the carrier frequency is ~1.5 GHz, even a 0.1 Hz Doppler shift corresponds to ~0.02 m/s velocity — far more precise than position-differencing.

Apple's CLLocation returns Doppler-derived speed when available. This is why speed from a good GPS fix at pedestrian velocities (1–2 m/s) can be accurate to ±0.3 m/s — useful for distinguishing 1.4 m/s walking from 0.5 m/s shuffle.

5.2 Step detection: peak-finding on the acceleration magnitude vector

The coprocessor's step detection algorithm (and CMPedometer) works by finding peaks in the acceleration magnitude signal. Here's a simplified version of the logic:

magnitude(t) = sqrt(ax(t)² + ay(t)² + az(t)²)

During walking, each footstrike causes the body's centre of mass to decelerate slightly, then accelerate again on toe-off. This produces a signature waveform in the magnitude signal:

Magnitude (g)
1.4 |        *       *
1.2 |      *   *   *   *
1.0 |    *       * *     *
0.8 |  *             *
    |______________________ time →
          ↑   ↑   ↑   ↑
        footstrikes (steps)

The frequency of these peaks is the cadence. Peak detection requires:

  1. Low-pass filtering to remove high-frequency noise (the coprocessor does this in hardware)
  2. Finding local maxima above a threshold (~1.2 g for the magnitude)
  3. Requiring a minimum time between peaks (~0.25 sec = max ~4 steps/sec) to avoid double-counting

The coprocessor also learns your personal gait signature through calibration (done automatically over time or manually via the Health app), which is why step count accuracy improves after the first few days of use.

5.3 Population variance as a motion roughness measure

The variance of a set of N values is:

σ² = (1/N) × Σ(xᵢ - μ)²

where μ = (1/N) × Σxᵢ  (the mean)

In Swift:

func variance(of values: [Double]) -> Double {
    guard values.count > 1 else { return 0 }
    let mean = values.reduce(0, +) / Double(values.count)
    let sumSquaredDeviations = values.reduce(0) { $0 + ($1 - mean) * ($1 - mean) }
    return sumSquaredDeviations / Double(values.count)
}

Applied to a rolling 50-sample (1-second at 50 Hz) window of acceleration magnitudes, this gives a scalar measure of how "rough" the motion is.

Why a rolling window rather than all-time variance?

A cumulative variance from the moment the app started would be dominated by historical data and would react very slowly to changes. A 1-second rolling window captures the current motion state. The choice of window length involves a trade-off:

  • Shorter window (0.5 sec / 25 samples): Responds faster to mode changes, but more susceptible to momentary spikes (a single pothole).
  • Longer window (2 sec / 100 samples): More stable classification, but slower to detect transitions from walking to driving.

A 1-second window at 50 Hz is a reasonable default for most applications.

Why magnitude rather than a single axis?

Using the magnitude sqrt(x² + y² + z²) makes the computation rotation-invariant. You don't need to know which way the phone is oriented. A step impulse appears as a magnitude spike regardless of whether the phone is in a jacket pocket, a trouser pocket, or held in the hand.

5.4 Why the M-series coprocessor uses a Hidden Markov Model

The coprocessor's activity classifier almost certainly uses a Hidden Markov Model (HMM), based on Apple patents and academic literature. Here's the intuition:

An HMM models a system that transitions between hidden states (the activities: walking, driving, etc.) over time. The key insight is that activities have temporal continuity — you're unlikely to switch from driving to walking in 0.1 seconds. The HMM encodes this via transition probabilities:

P(driving → walking in 1 sec) ≈ 0.001  (very unlikely)
P(driving → driving in 1 sec) ≈ 0.999  (very likely)
P(walking → running in 1 sec) ≈ 0.05   (possible)

At each time step, the model observes the sensor data (the "emission") and updates its belief about the current hidden state using Bayes' rule:

P(state | observations) ∝ P(observations | state) × P(state | previous_state)

This is what makes the coprocessor robust to brief ambiguous readings. A single bumpy-road sample that looks like walking variance won't flip the classifier to "walking" if the transition probability from automotive to walking is very low and GPS is reading 80 km/h.

The HMM's parameters (transition probabilities and observation distributions) are trained offline on large datasets of labelled activities collected from real users. Apple's implementation also appears to do online adaptation — the classifier personalises to your gait over time, which is why the system becomes more accurate after a few days.


6. Full Implementation Walkthrough

6.1 Project setup and permissions

Add entitlements and plist keys:

In Info.plist:

<!-- Required for CLLocationManager -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to detect whether you are walking or driving.</string>

<!-- Required only if you need location in the background -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Background location allows activity detection to continue when the app is minimised.</string>

<!-- Required for CMMotionActivityManager -->
<key>NSMotionUsageDescription</key>
<string>Motion data lets us distinguish walking from driving without draining your battery.</string>

Background Modes (if you need detection while app is in background):

In Signing & CapabilitiesBackground Modes:

  • ✅ Location updates (for GPS-based detection)
  • ✅ Motion and Fitness (automatically enabled by CoreMotion)

Note: CMMotionActivityManager and CMPedometer continue running through the coprocessor regardless of background modes. Only CMMotionManager (raw accelerometer) stops in the background unless you have a specific background entitlement.

Minimum deployment target: iOS 11+ is required for all APIs used. currentCadence on CMPedometer requires iOS 9+. The M-series motion coprocessor is available from iPhone 5s (2013) onward.

6.2 ActivityType and ActivityResult models

import Foundation

/// The classified activity type
enum ActivityType: String, CaseIterable {
    case walking    = "🚶 Walking"
    case driving    = "🚗 Driving"
    case running    = "🏃 Running"
    case cycling    = "🚴 Cycling"
    case stationary = "🧍 Stationary"
    case unknown    = "❓ Unknown"
    
    /// Human-readable description without emoji
    var plainName: String {
        switch self {
        case .walking:    return "Walking"
        case .driving:    return "Driving"
        case .running:    return "Running"
        case .cycling:    return "Cycling"
        case .stationary: return "Stationary"
        case .unknown:    return "Unknown"
        }
    }
    
    /// Whether this activity type involves self-propulsion on foot
    var isPedestrian: Bool {
        return self == .walking || self == .running
    }
    
    /// Whether this activity suggests the user is inside a vehicle
    var isVehicular: Bool {
        return self == .driving
    }
}

/// A single classified activity observation with all supporting signal values
struct ActivityResult {
    let type: ActivityType
    let confidence: String          // "low", "medium", or "high"
    let speed: Double               // m/s from GPS (-1 if unavailable)
    let stepFrequency: Double       // steps/sec from pedometer (0 if not walking)
    let accelerationVariance: Double // dimensionless variance of magnitude buffer
    let timestamp: Date
    
    /// Speed converted to km/h for display
    var speedKMH: Double {
        return speed >= 0 ? speed * 3.6 : -1
    }
    
    /// Confidence expressed as a 0–1 numeric weight (for downstream weighting)
    var confidenceWeight: Double {
        switch confidence {
        case "high":   return 1.0
        case "medium": return 0.6
        default:       return 0.3
        }
    }
    
    /// Debug description
    var debugDescription: String {
        let speedStr = speed >= 0 ? String(format: "%.1f km/h", speedKMH) : "unavailable"
        return """
        ─────────────────────────────────────
        Activity   : \(type.rawValue)
        Confidence : \(confidence)
        GPS speed  : \(speedStr)
        Step freq  : \(String(format: "%.2f", stepFrequency)) steps/s
        Accel var  : \(String(format: "%.4f", accelerationVariance))
        Time       : \(timestamp)
        ─────────────────────────────────────
        """
    }
}

6.3 ActivityDetector class structure

import CoreMotion
import CoreLocation
import Foundation

class ActivityDetector: NSObject {

    // ── CoreMotion ────────────────────────────────────────────────────────────
    
    /// High-level coprocessor activity classifier
    private let motionActivityManager = CMMotionActivityManager()
    
    /// Step counting and cadence
    private let pedometer = CMPedometer()
    
    /// Raw accelerometer stream
    private let motionManager = CMMotionManager()
    
    // ── CoreLocation ──────────────────────────────────────────────────────────
    
    /// GPS speed and position
    private let locationManager = CLLocationManager()
    
    // ── Rolling signal state ──────────────────────────────────────────────────
    
    /// Last valid GPS speed in m/s; -1 if no valid reading yet
    private var currentGPSSpeed: Double = -1
    
    /// Current step frequency in steps/sec from pedometer
    private var currentStepFrequency: Double = 0
    
    /// Rolling buffer of acceleration magnitude samples (1 second at 50 Hz)
    private var recentAccelSamples: [Double] = []
    private let sampleBufferMax = 50
    
    /// Derived variance of the rolling buffer
    private var currentAccelVariance: Double = 0
    
    // ── Output ────────────────────────────────────────────────────────────────
    
    /// Called every time a new fused classification is produced.
    /// Invoked on the main thread.
    var onActivityDetected: ((ActivityResult) -> Void)?
    
    // ── Lifecycle ─────────────────────────────────────────────────────────────
    
    func startDetecting() {
        requestPermissions()
        startHighLevelActivityMonitoring()
        startPedometerUpdates()
        startRawAccelerometer()
        startGPS()
    }
    
    func stopDetecting() {
        motionActivityManager.stopActivityUpdates()
        pedometer.stopUpdates()
        motionManager.stopAccelerometerUpdates()
        locationManager.stopUpdatingLocation()
    }
}

6.4 Signal 1: CMMotionActivityManager

private func startHighLevelActivityMonitoring() {
    guard CMMotionActivityManager.isActivityAvailable() else {
        // Pre-M7 devices (iPhone 5 and earlier) don't have the coprocessor.
        // In practice this is only relevant if you support iOS 11 on very old hardware.
        return
    }
    
    motionActivityManager.startActivityUpdates(to: .main) { [weak self] activity in
        guard let self = self, let activity = activity else { return }
        
        // This callback fires when the coprocessor detects an activity change.
        // It is NOT fired at a fixed rate — only on transitions.
        let classified = self.classifyHighLevel(activity)
        
        // Log to console for debugging
        print("[CMMotionActivity] \(classified.rawValue)  confidence: \(activity.confidence)")
        
        // Note: we don't call evaluateAndReport() here because
        // CMMotionActivityManager alone is our sanity check, not the
        // primary fused decision-maker. You could add it here if desired.
    }
}

/// Translates CMMotionActivity boolean flags to our ActivityType enum.
/// Priority order: automotive > running > walking > cycling > stationary
private func classifyHighLevel(_ a: CMMotionActivity) -> ActivityType {
    // automotive flag is most distinct — don't deprioritise it
    if a.automotive { return .driving    }
    if a.running    { return .running    }
    if a.walking    { return .walking    }
    if a.cycling    { return .cycling    }
    if a.stationary { return .stationary }
    return .unknown
}

Why prioritise automotive above walking?

In transition moments (getting into a car, stepping onto a moving train platform), multiple flags can be true. automotive is the correct dominant classification in those cases because it means the user is inside a vehicle, regardless of incidental walking motion detected.

6.5 Signal 2: CMPedometer

private var currentStepFrequency: Double = 0

private func startPedometerUpdates() {
    guard CMPedometer.isStepCountingAvailable() else {
        print("CMPedometer: step counting unavailable on this device")
        return
    }
    
    // startUpdates fires as steps accumulate; the interval is not fixed.
    // Typical update rate: every 1–2 steps.
    pedometer.startUpdates(from: Date()) { [weak self] data, error in
        guard let self = self, let data = data, error == nil else { return }
        
        // Prefer currentCadence (direct steps/sec) over deriving from currentPace
        if let cadence = data.currentCadence {
            self.currentStepFrequency = cadence.doubleValue
        } else if let pace = data.currentPace {
            // pace is seconds/metre; to get steps/sec:
            //   steps/sec = stride_length_m / seconds_per_step
            // Without stride length we approximate:
            //   seconds_per_step ≈ pace (this is actually sec/m but works as proxy)
            self.currentStepFrequency = 1.0 / pace.doubleValue
        }
        
        // Trigger a new fused evaluation
        DispatchQueue.main.async {
            self.evaluateAndReport()
        }
    }
}

Why does currentPace approximate seconds_per_step reasonably well?

The pedometer's stride length model estimates stride length from cadence and historical calibration. For an average adult at normal walking speed, stride length is approximately 0.75 m. currentPace (sec/metre) ÷ 0.75 gives seconds/step. When used just for the threshold check (is cadence > 1.2?), this approximation is sufficient.

6.6 Signal 3: Raw accelerometer variance

private var currentAccelVariance: Double = 0

private func startRawAccelerometer() {
    guard motionManager.isAccelerometerAvailable else { return }
    
    // 50 Hz is a good balance: captures the ~1.5–2 Hz walking frequency
    // well above Nyquist (which requires at least 4 Hz sample rate for a 2 Hz signal),
    // while not overwhelming the main queue with updates.
    motionManager.accelerometerUpdateInterval = 1.0 / 50.0
    
    motionManager.startAccelerometerUpdates(to: .main) { [weak self] data, error in
        guard let self = self, let data = data, error == nil else { return }
        
        // Compute orientation-invariant acceleration magnitude
        let x = data.acceleration.x
        let y = data.acceleration.y
        let z = data.acceleration.z
        let magnitude = sqrt(x*x + y*y + z*z)
        
        // Maintain a rolling 1-second buffer
        self.recentAccelSamples.append(magnitude)
        if self.recentAccelSamples.count > self.sampleBufferMax {
            self.recentAccelSamples.removeFirst()  // O(n) — use a ring buffer for production
        }
        
        // Recompute variance over the buffer
        self.currentAccelVariance = self.variance(of: self.recentAccelSamples)
        
        // Note: we don't call evaluateAndReport() on every accelerometer sample
        // (50 times/sec is too frequent). Pedometer and GPS callbacks drive reporting.
    }
}

/// Population variance: σ² = (1/N) Σ(xᵢ - μ)²
private func variance(of values: [Double]) -> Double {
    guard values.count > 1 else { return 0.0 }
    let count = Double(values.count)
    let mean = values.reduce(0.0, +) / count
    let sumSquaredDeviations = values.reduce(0.0) { accumulator, value in
        let deviation = value - mean
        return accumulator + deviation * deviation
    }
    return sumSquaredDeviations / count
}

Production optimisation — ring buffer:

The removeFirst() call on an array is O(n) because it shifts all remaining elements. For a 50-element buffer at 50 Hz this is negligible, but for higher sample rates, use ArraySlice or a proper ring buffer:

// Simple ring buffer alternative
private var accelRingBuffer = [Double](repeating: 0, count: 50)
private var accelWriteIndex = 0

// In the update handler:
accelRingBuffer[accelWriteIndex % sampleBufferMax] = magnitude
accelWriteIndex += 1

// Variance computation still uses accelRingBuffer[0..<sampleBufferMax]

6.7 Signal 4: GPS speed via CLLocationManager

private func requestPermissions() {
    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
}

private func startGPS() {
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    // Minimum distance filter: update every 2 metres.
    // This prevents callbacks when standing still due to GPS noise oscillation.
    locationManager.distanceFilter  = 2.0
    locationManager.startUpdatingLocation()
}

// CLLocationManagerDelegate
extension ActivityDetector: CLLocationManagerDelegate {
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse, .authorizedAlways:
            manager.startUpdatingLocation()
        case .denied, .restricted:
            // Degrade gracefully: rely only on motion sensors
            print("Location access denied — GPS speed unavailable")
        default:
            break
        }
    }
    
    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        // Use the most recent location
        guard let location = locations.last else { return }
        
        // Reject readings where the speed measurement is invalid.
        // speedAccuracy < 0 indicates the speed is not available for this fix.
        guard location.speedAccuracy >= 0 else { return }
        
        // CLLocation.speed can technically be -1 for no measurement;
        // clamp to 0 to avoid negative speeds entering our logic.
        currentGPSSpeed = max(0.0, location.speed)
        
        // A new GPS reading triggers a fused evaluation
        evaluateAndReport()
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        guard let clError = error as? CLError else { return }
        switch clError.code {
        case .denied:
            print("GPS denied by user")
        case .locationUnknown:
            // Transient — GPS is trying to acquire a fix
            currentGPSSpeed = -1
        default:
            print("GPS error: \(clError.localizedDescription)")
        }
    }
}

6.8 The fusion classifier — vote-based decision logic

private func evaluateAndReport() {
    let speed    = currentGPSSpeed        // m/s  (-1 = unavailable)
    let stepFreq = currentStepFrequency   // steps/sec
    let accelVar = currentAccelVariance   // dimensionless
    
    let (type, confidence) = fuseSignals(
        speed: speed,
        stepFrequency: stepFreq,
        accelVariance: accelVar
    )
    
    let result = ActivityResult(
        type: type,
        confidence: confidence,
        speed: speed,
        stepFrequency: stepFreq,
        accelerationVariance: accelVar,
        timestamp: Date()
    )
    
    onActivityDetected?(result)
}

private func fuseSignals(
    speed: Double,
    stepFrequency: Double,
    accelVariance: Double
) -> (ActivityType, String) {
    
    // ── Thresholds (tuneable) ─────────────────────────────────────────────
    let drivingSpeedThreshold: Double = 6.0    // m/s  ≈ 21.6 km/h ≈ 13.4 mph
    let walkingSpeedMax:       Double = 2.2    // m/s  ≈ 7.9  km/h ≈ 4.9  mph
    let runningSpeedMin:       Double = 2.2    // m/s
    
    let walkingStepMin: Double = 1.2           // steps/sec (slow walk)
    let runningStepMin: Double = 2.5           // steps/sec (jog boundary)
    let noStepThreshold: Double = 0.2          // steps/sec (essentially zero)
    
    let drivingAccelVarMax: Double = 0.02      // very smooth ride
    let walkingAccelVarMin: Double = 0.02      // rhythmic step bounce
    // ─────────────────────────────────────────────────────────────────────
    
    var votes: [ActivityType: Int] = [:]
    
    // ── Vote 1: GPS speed (weight 3 for driving, 2 for others) ────────────
    // GPS speed is the most definitive signal when reliable.
    if speed >= 0 {
        if speed > drivingSpeedThreshold {
            votes[.driving, default: 0] += 3  // very strong: no pedestrian moves this fast
        } else if speed < walkingSpeedMax {
            votes[.walking, default: 0] += 2  // probably walking or slow
        } else {
            // Speed is in the 2.2–6.0 range: could be fast walk, run, or slow cycle
            votes[.running, default: 0] += 2
        }
    }
    // If speed < 0 (unavailable), this entire block is skipped —
    // the other two signals carry the classification alone.
    
    // ── Vote 2: Step frequency (weight 2) ─────────────────────────────────
    // Cadence is a clean binary: you're either stepping or you're not.
    if stepFrequency > runningStepMin {
        votes[.running, default: 0]  += 2
    } else if stepFrequency > walkingStepMin {
        votes[.walking, default: 0]  += 2
    } else if stepFrequency < noStepThreshold {
        // Near-zero cadence: driving or stationary
        votes[.driving, default: 0]  += 1
    }
    
    // ── Vote 3: Accelerometer variance (weight 1) ──────────────────────────
    // Secondary evidence — corroborates but doesn't decide.
    if accelVariance < drivingAccelVarMax {
        votes[.driving, default: 0] += 1  // smooth: consistent with a vehicle
    } else if accelVariance > walkingAccelVarMin {
        votes[.walking, default: 0] += 1  // bouncy: consistent with walking
    }
    
    // ── Tally ──────────────────────────────────────────────────────────────
    let winner = votes.max { $0.value < $1.value }
    let type   = winner?.key   ?? .unknown
    let score  = winner?.value ?? 0
    
    // Map total vote score to confidence
    let confidence: String
    switch score {
    case 5...:  confidence = "high"     // ≥2 signals strongly agree
    case 3...4: confidence = "medium"   // majority signal agrees
    default:    confidence = "low"      // weak or conflicting signals
    }
    
    return (type, confidence)
}

How the vote scores map to scenarios:

ScenarioGPS votesCadence votesVar votesTotalConfidence
Walking, GPS available2 (walk)2 (walk)1 (walk)5 walkHigh
Driving at 60 km/h3 (drive)1 (drive)1 (drive)5 driveHigh
Traffic jam (5 km/h)2 (walk)1 (drive)1 (drive)2 walk, 2 driveLow — tie
No GPS, walking2 (walk)1 (walk)3 walkMedium
Running2 (run)2 (run)1 (walk)4 run, 1 walkMedium-high

Notice the traffic jam tie case: score is 2 each, resulting in low confidence and potentially .unknown. This is the correct behaviour — we genuinely don't know. Adding the CMMotionActivityManager high-level classification as a tie-breaker is a recommended enhancement (see Section 11).

6.9 Wiring it into a ViewController

import UIKit

class ActivityViewController: UIViewController {
    
    private let detector = ActivityDetector()
    
    // ── UI outlets ────────────────────────────────────────────────────────────
    @IBOutlet private weak var activityLabel:      UILabel!
    @IBOutlet private weak var confidenceLabel:    UILabel!
    @IBOutlet private weak var speedLabel:         UILabel!
    @IBOutlet private weak var cadenceLabel:       UILabel!
    @IBOutlet private weak var varianceLabel:      UILabel!
    @IBOutlet private weak var timestampLabel:     UILabel!
    
    // ── Lifecycle ─────────────────────────────────────────────────────────────
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureDetector()
        detector.startDetecting()
    }
    
    deinit {
        detector.stopDetecting()
    }
    
    // ── Setup ─────────────────────────────────────────────────────────────────
    
    private func configureDetector() {
        detector.onActivityDetected = { [weak self] result in
            // Callback is already on main thread
            self?.updateUI(with: result)
            self?.handleActivityChange(result)
        }
    }
    
    // ── UI update ─────────────────────────────────────────────────────────────
    
    private func updateUI(with result: ActivityResult) {
        activityLabel.text   = result.type.rawValue
        confidenceLabel.text = "Confidence: \(result.confidence)"
        
        if result.speed >= 0 {
            speedLabel.text = String(format: "Speed: %.1f km/h", result.speedKMH)
        } else {
            speedLabel.text = "Speed: GPS unavailable"
        }
        
        cadenceLabel.text  = String(format: "Cadence: %.2f steps/s", result.stepFrequency)
        varianceLabel.text = String(format: "Variance: %.4f", result.accelerationVariance)
        
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        timestampLabel.text = "Updated: \(formatter.string(from: result.timestamp))"
    }
    
    // ── Application logic ─────────────────────────────────────────────────────
    
    private func handleActivityChange(_ result: ActivityResult) {
        guard result.confidence != "low" else { return }
        
        switch result.type {
        case .driving:
            enableDrivingMode()
        case .walking, .running:
            enablePedestrianMode()
        case .stationary:
            enableStationaryMode()
        default:
            break
        }
    }
    
    private func enableDrivingMode() {
        // Example: suppress notifications, switch to audio-only interface
        print("Driving mode activated")
    }
    
    private func enablePedestrianMode() {
        // Example: switch to walking navigation, show nearby POIs
        print("Pedestrian mode activated")
    }
    
    private func enableStationaryMode() {
        // Example: offer suggestions based on location
        print("Stationary mode activated")
    }
}

7. Scenario Walkthroughs

7.1 Scenario A: Normal sidewalk walk

Context: User walking to work at 1.5 m/s on a clear day, phone in jacket pocket.

SignalValueVote
GPS speed1.5 m/s (valid, accuracy ±0.3 m/s)+2 walking
Cadence1.7 steps/sec+2 walking
Accel variance0.045+1 walking
Total5 walking — High confidence

CMMotionActivityManager fires .walking with .high confidence simultaneously.

Result: ActivityType.walking, confidence "high"


7.2 Scenario B: Driving on a motorway

Context: User driving at 110 km/h (30.5 m/s) on a motorway, phone on dashboard mount.

SignalValueVote
GPS speed30.5 m/s (valid)+3 driving
Cadence0.0 steps/sec+1 driving
Accel variance0.008 (smooth tarmac)+1 driving
Total5 driving — High confidence

Result: ActivityType.driving, confidence "high"


7.3 Scenario C: Sitting in a slow-moving traffic jam

Context: Car moving at 0.8 m/s through city traffic, phone in cupholder.

SignalValueVote
GPS speed0.8 m/s+2 walking (below walkingSpeedMax of 2.2)
Cadence0.0 steps/sec+1 driving
Accel variance0.015 (small bumps)borderline — 0 votes
Total2 walking, 1 driving — Low confidence

Result: Type could be walking or driving; confidence "low". This is expected — GPS is ambiguous. The CMMotionActivityManager should return .automotive here (the coprocessor's HMM knows the user was driving before entering the jam) and can be used as a tie-breaker.

Recommended enhancement: Add a fourth vote source from CMMotionActivityManager:

// In the fusion classifier, add:
if let lastCoprocessorActivity = lastCoprocessorActivity {
    if lastCoprocessorActivity.automotive {
        votes[.driving, default: 0] += 2
    } else if lastCoprocessorActivity.walking {
        votes[.walking, default: 0] += 2
    }
}

7.4 Scenario D: Passenger in a car (not driving)

From a sensor perspective, a passenger is indistinguishable from a driver. Both are inside an automotive vehicle, both show near-zero cadence, and GPS speed reflects the vehicle speed. This is a fundamental limitation of the approach described here.

To distinguish driver from passenger requires additional signals:

  • Bluetooth/CarPlay connection — if the phone is connected to the car's infotainment system, it's likely the driver's phone.
  • Steering wheel pattern in gyroscope — the driver's phone (in a cup holder or mount near the steering wheel) shows micro-rotational signals correlated with steering.
  • Seat position — not accessible to apps without additional hardware.

Apple's own "Driving Focus" uses CarPlay connection and Bluetooth as the primary discriminator.


7.5 Scenario E: Running for a bus

Context: User sprinting at 4.5 m/s for 15 seconds, then standing at a bus stop.

During the sprint:

SignalValueVote
GPS speed4.5 m/s+2 running
Cadence3.2 steps/sec+2 running
Accel variance0.22+1 walking
Total4 running, 1 walking — Medium confidence running

At the bus stop (after 5 seconds standing):

SignalValueVote
GPS speed0.1 m/s (GPS noise at standstill)+2 walking
Cadence0.0+1 driving
Accel variance0.003 (very still)+1 driving
Total2 walking, 2 driving — Low confidence (tie)

The tie at the bus stop can be resolved by the coprocessor (which will return .stationary after a few seconds of no movement) or by a temporal hysteresis filter that prevents rapid transitions.


7.6 Scenario F: GPS-denied environment (underground car park)

Context: User walking through an underground car park. GPS unavailable, speed returns -1.

SignalValueVote
GPS speed-1 (invalid)No votes cast
Cadence1.6 steps/sec+2 walking
Accel variance0.040+1 walking
Total3 walking — Medium confidence

GPS votes are skipped entirely (speed >= 0 check fails). The system correctly falls back to the motion-only signals. Medium confidence reflects the missing GPS corroboration.

This demonstrates why multi-signal fusion is essential: GPS-only classification would produce no output at all in this scenario.


7.7 Scenario G: Cycling

Context: User cycling at 6 m/s on a bike path, phone in jersey pocket.

SignalValueVote
GPS speed6.0 m/sExactly at drivingSpeedThreshold — votes +3 driving
Cadence0.0 steps/sec (no footsteps on pedals)+1 driving
Accel variance0.018 (smooth bike path)borderline — 0 votes
Total4 driving — Medium confidence

This is a known misclassification: cycling at road speed is mistaken for driving. The CMMotionActivityManager returns .cycling here, which can override the fusion result. This highlights the value of including the coprocessor result as a voting signal.

Enhanced fusion including coprocessor:

// Add to fuseSignals:
if let coprocessorType = lastCoprocessorClassification {
    switch coprocessorType {
    case .cycling:
        votes[.cycling, default: 0] += 3  // Trust the coprocessor on cycling
    case .driving:
        votes[.driving, default: 0] += 2
    case .walking:
        votes[.walking, default: 0] += 2
    default:
        break
    }
}

7.8 Scenario H: Treadmill walking

Context: User walking on a treadmill at 5 km/h. GPS speed: effectively 0 (not moving through space).

SignalValueVote
GPS speed~0.1 m/s (GPS noise)+2 walking
Cadence1.6 steps/sec+2 walking
Accel variance0.035+1 walking
Total5 walking — High confidence

The treadmill case is handled correctly because GPS is not needed — cadence and variance alone confirm walking. The Health app also correctly logs treadmill walks as walking distance (using step count + stride length rather than GPS displacement).


8. Edge Cases and Known Failure Modes

False walking during car vibration

On cobblestone streets, gravel roads, or over expansion joints on bridges, a car can produce accelerometer variance of 0.05–0.15 — overlapping with the walking range. The cadence signal will remain at zero (no steps), which should prevent misclassification if weighted properly. Ensure cadence has higher weight than variance in your fusion logic.

False driving during wheelchair use

A manual wheelchair user at normal push speed (~1–2 m/s) has near-zero cadence (the arms push, not walk) and may have low accelerometer variance on a smooth floor. GPS correctly reads walking speed, but cadence and variance both vote "driving/stationary". The CMMotionActivityManager typically returns .stationary for wheelchair users on smooth floors, which is technically incorrect.

If your app needs to support wheelchair users, consider adding an explicit wheelchair mode (as Apple's Health app does) with separate thresholds.

False stationary during slow dance or yoga

Some yoga poses involve holding the body still for seconds. Between transitions, the sensor profile can briefly match "stationary". This is usually benign (the transition lasts 1–2 seconds) but can cause flickers in real-time display. Hysteresis (Section 11.3) is the remedy.

Running at low cadence (injury or age)

Elderly users and some injury-recovery patients may run or jog at cadences below 2.5 steps/sec. The classifier would label these as "walking" — usually acceptable since the distinction is minor for most applications.

Child in pushchair

A child's phone in a pushchair shows: GPS at walking speed, near-zero cadence, and moderate variance from the pushchair's vibration. Classification correctly returns "walking" from the parent's perspective (the pushchair is being propelled by a walking adult). The child's own motion (sitting still) is not separately classifiable.

Elevator

Vertical motion (elevator) produces a brief acceleration spike when the elevator starts and stops, and a short period of near-zero horizontal displacement. GPS may not update (indoors). The system typically classifies elevator rides as "stationary" — correct enough for most applications, but CMDeviceMotion.userAcceleration on the Y-axis can detect the vertical acceleration if needed.


9. Real-World Use Cases and Applications

These apps switch the UI, route type, and turn-by-turn style automatically based on detected activity. When the system detects walking, it may:

  • Show pedestrian routing (footpaths, crosswalks)
  • Reduce map zoom to show more detail at the walking scale
  • Announce turns in seconds/distance rather than motorway-style distance markers

Driving Focus / Do Not Disturb While Driving

iOS's built-in Driving Focus uses the motion stack to automatically enable notification suppression. It responds to CMMotionActivityManager returning .automotive with high confidence, optionally combined with CarPlay/Bluetooth detection.

Fitness and health tracking (Apple Health, Strava, Garmin Connect)

Walking and running activity detected by CMPedometer accumulates in HealthKit under HKQuantityType.stepCount, HKQuantityType.distanceWalkingRunning, and HKQuantityType.activeEnergyBurned. Apps can query and write these quantities:

import HealthKit

let store = HKHealthStore()
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!

let query = HKStatisticsQuery(
    quantityType: stepType,
    quantitySamplePredicate: nil,
    options: .cumulativeSum
) { _, result, error in
    let steps = result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
    print("Total steps: \(steps)")
}

store.execute(query)

Insurance telematics

Several car insurance providers (Progressive Snapshot, Allstate Drivewise) use smartphone sensors to score driving behaviour. Activity detection ensures the telematics app only analyses driving sessions and ignores walking or public transport. Accelerometer data during confirmed driving sessions is used to detect harsh braking (large negative acceleration spike), rapid cornering (lateral acceleration), and speeding (GPS speed above limit).

Automatic mileage logging

Apps like MileIQ, Driversnote, and Everlance automatically log vehicle trips for expense reporting or tax deduction. Activity detection is the trigger: when the transition from walking to driving is detected, a new trip record begins. When driving transitions to walking (arriving at a destination), the trip ends and a GPS track is stored.

Adaptive UI for accessibility

Apps aimed at elderly users can use activity detection to adjust interface behaviour:

  • When walking: increase notification urgency (user may not check phone)
  • When stationary: show detailed interactive content
  • When driving: suppress all non-critical interruptions

Location-based services optimisation

Continuous GPS use at kCLLocationAccuracyBest drains ~20–30 mA. An app can use activity detection to reduce GPS accuracy when walking or stationary:

detector.onActivityDetected = { [weak self] result in
    guard let self = self else { return }
    
    switch result.type {
    case .driving:
        // High accuracy needed for navigation
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
        self.locationManager.distanceFilter  = 5
    case .walking:
        // Moderate accuracy sufficient
        self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
        self.locationManager.distanceFilter  = 10
    case .stationary:
        // Minimal accuracy saves battery
        self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        self.locationManager.distanceFilter  = 50
    default:
        break
    }
}

Geofencing smarter triggers

Standard CLCircularRegion geofences fire based on GPS entry/exit regardless of activity. Combining with activity detection enables smarter triggers: "notify when the user arrives at work and is walking (i.e. they've parked and are approaching on foot)" rather than firing while still in the car.


10. Battery and Performance Considerations

ComponentApproximate current drainNotes
CMMotionActivityManager~0.1 mARuns on coprocessor; negligible
CMPedometer~0.1 mACoprocessor-assisted
CMMotionManager (50 Hz)~0.5–1 mAMain CPU required; stops in background
CLLocationManager (Best accuracy)~20–30 mAHighest drain; use only when needed
CLLocationManager (100m accuracy)~3–5 mAAcceptable for coarse activity
Total (full stack)~22–32 mAGPS dominates

Practical battery-saving strategies:

Use CMMotionActivityManager first. If you only need to know "is the user driving or walking?" at a coarse level, CMMotionActivityManager alone costs essentially nothing. Add GPS only when you need precise speed or route data.

Adaptive GPS accuracy (shown in Section 9 above). Reduce GPS accuracy when the user is not driving.

Stop the raw accelerometer when backgrounded. CMMotionManager stops automatically when the app backgrounds. If you need background variance analysis, consider using CMPedometer.queryPedometerData(from:to:withHandler:) batch queries rather than live streaming.

Batch historical queries instead of live streaming. For apps that analyse yesterday's activity pattern (e.g., a fitness summary), use queryActivityStarting(from:to:to:withHandler:) rather than streaming all day.

Implement significantLocationChangeMonitoring for coarse triggers:

locationManager.startMonitoringSignificantLocationChanges()
// Fires only when cell tower changes (~500m accuracy, ~1 mA)
// Enough to detect a major travel context change

11. Improving Accuracy: Advanced Techniques

11.1 Kalman filtering for GPS speed smoothing

Raw GPS speed can jump between updates due to multipath errors. A simple 1D Kalman filter smooths these spikes:

class KalmanFilter {
    private var estimate: Double
    private var errorCovariance: Double
    private let processNoise: Double     // How much we trust the model
    private let measurementNoise: Double // How much we trust the sensor

    init(initial: Double, processNoise: Double = 0.1, measurementNoise: Double = 1.0) {
        self.estimate = initial
        self.errorCovariance = 1.0
        self.processNoise = processNoise
        self.measurementNoise = measurementNoise
    }

    func update(measurement: Double) -> Double {
        // Predict
        let predictedCovariance = errorCovariance + processNoise

        // Update (Kalman gain)
        let gain = predictedCovariance / (predictedCovariance + measurementNoise)
        estimate = estimate + gain * (measurement - estimate)
        errorCovariance = (1 - gain) * predictedCovariance

        return estimate
    }
}

// Usage
let speedFilter = KalmanFilter(initial: 0, processNoise: 0.05, measurementNoise: 0.5)

// In location update handler:
let rawSpeed = max(0, location.speed)
let filteredSpeed = speedFilter.update(measurement: rawSpeed)

Tuning the filter:

  • Higher processNoise → filter trusts speed changes more (less smoothing, faster response)
  • Higher measurementNoise → filter trusts GPS less (more smoothing, slower response)
  • For walking: processNoise = 0.05, measurementNoise = 0.3 (walking speed changes slowly)
  • For driving: processNoise = 0.5, measurementNoise = 0.5 (speed changes more frequently)

11.2 Frequency-domain analysis (FFT) on accelerometer data

The walking frequency (~1.4–2.5 Hz) is distinct from driving vibration frequencies (engine ~10–100 Hz, road rumble ~5–20 Hz). A Fast Fourier Transform on the accelerometer magnitude signal can identify these frequency bands and provide a much cleaner classifier.

import Accelerate

func dominantFrequency(samples: [Double], sampleRate: Double) -> Double {
    let n = samples.count
    let log2n = vDSP_Length(log2(Double(n)))
    
    guard let fftSetup = vDSP_create_fftsetupD(log2n, FFTRadix(kFFTRadix2)) else {
        return 0
    }
    defer { vDSP_destroy_fftsetupD(fftSetup) }
    
    // Set up complex buffer
    var realParts = samples
    var imagParts = [Double](repeating: 0, count: n)
    
    var splitComplex = DSPDoubleSplitComplex(
        realp: &realParts,
        imagp: &imagParts
    )
    
    // Execute FFT
    vDSP_fft_zipD(fftSetup, &splitComplex, 1, log2n, FFTDirection(kFFTDirection_Forward))
    
    // Compute magnitudes
    var magnitudes = [Double](repeating: 0, count: n / 2)
    vDSP_zvmagsD(&splitComplex, 1, &magnitudes, 1, vDSP_Length(n / 2))
    
    // Find peak frequency (excluding DC component at index 0)
    let peakIndex = magnitudes[1...].enumerated().max(by: { $0.element < $1.element })?.offset ?? 0
    let peakFrequency = Double(peakIndex + 1) * sampleRate / Double(n)
    
    return peakFrequency
}

// Walking peak: 1.4–2.5 Hz
// Driving peak: often 10–80 Hz from engine/road
// If peakFrequency is between 1.0 and 3.0: strong walking indicator

This approach is more computationally expensive but dramatically reduces false positives from bumpy roads. The Accelerate framework's vDSP FFT runs efficiently on the iPhone's NEON SIMD unit.

11.3 Hysteresis to prevent rapid mode-switching

Without hysteresis, a user stopping at a red light can trigger multiple walking↔driving transitions per second as GPS fluctuates around the threshold. Hysteresis requires that a new classification be sustained for a minimum duration before committing to it:

class HystereticActivityFilter {
    private var currentType: ActivityType = .unknown
    private var pendingType: ActivityType = .unknown
    private var pendingStartTime: Date?
    
    /// Minimum duration (seconds) a new activity must be consistently detected
    /// before we commit to it.
    let hysteresisWindow: TimeInterval
    
    init(hysteresisWindow: TimeInterval = 5.0) {
        self.hysteresisWindow = hysteresisWindow
    }
    
    /// Feed in a new raw classification. Returns the stable classification.
    func filter(rawType: ActivityType) -> ActivityType {
        let now = Date()
        
        if rawType == currentType {
            // Consistent with current state — reset pending
            pendingType = rawType
            pendingStartTime = nil
            return currentType
        }
        
        if rawType == pendingType {
            // Same as pending — check if it's been long enough
            if let startTime = pendingStartTime,
               now.timeIntervalSince(startTime) >= hysteresisWindow {
                currentType = rawType
                pendingType = rawType
                pendingStartTime = nil
            }
        } else {
            // New type — start the hysteresis timer
            pendingType = rawType
            pendingStartTime = now
        }
        
        return currentType
    }
}

// Usage
let hystFilter = HystereticActivityFilter(hysteresisWindow: 5.0)

detector.onActivityDetected = { result in
    let stableType = hystFilter.filter(rawType: result.type)
    print("Stable activity: \(stableType)")
}

A 5-second window is appropriate for most applications. For driving mode detection specifically (where a false disable while stopped at a light is very disruptive), 10–15 seconds is more comfortable.

11.4 Machine learning with Create ML and CoreML

Apple's Create ML (Create ML.app in Xcode) includes an Activity Classifier template that accepts time-series sensor data and produces a CoreML model you can bundle into your app.

Data preparation:

Dataset structure:
data/
  walking/
    session_001.csv   (timestamp, ax, ay, az, gx, gy, gz)
    session_002.csv
    ...
  driving/
    session_001.csv
    ...
  running/
    session_001.csv
    ...

Each CSV row: a timestamp plus 6 columns of accelerometer and gyroscope data at a fixed sample rate (typically 50 Hz).

Training in Create ML:

  1. Open Create ML app in Xcode
  2. Create new Activity Classifier project
  3. Set prediction window size: 50 samples (1 second at 50 Hz)
  4. Drag in your labelled data folders
  5. Train — Create ML automatically extracts features (mean, variance, FFT peaks, etc.)
  6. Export the .mlmodel file

Using the model in your app:

import CoreML

// The generated class name comes from your Create ML project name
let model = try! ActivityClassifier(configuration: MLModelConfiguration())

// Collect 50 samples (1 second at 50 Hz) into a feature provider
// Create ML expects a MLMultiArray with shape [window, channels]
let windowSize = 50
var accelWindow: [[Double]] = []  // collect samples...

// Create MLMultiArray
let inputArray = try! MLMultiArray(
    shape: [NSNumber(value: windowSize), 6],  // 6 channels: ax,ay,az,gx,gy,gz
    dataType: .double
)

// Fill the array with your window of samples
for (i, sample) in accelWindow.enumerated() {
    for (j, value) in sample.enumerated() {
        inputArray[[NSNumber(value: i), NSNumber(value: j)]] = NSNumber(value: value)
    }
}

// Run inference
let input = ActivityClassifierInput(features: inputArray, stateIn: model.stateIn)
let output = try! model.prediction(input: input)

print("Predicted activity: \(output.label)")
print("Probabilities: \(output.labelProbability)")

The CoreML model runs at under 1ms per inference on modern iPhones using the Neural Engine. This approach can achieve >95% accuracy on the walking/driving distinction across a diverse population when trained on sufficient data (>1000 labelled sessions per class).


12. Privacy, Permissions, and App Store Compliance

Required permission descriptions

At minimum, your Info.plist must include:

<!-- Motion data — required for CMMotionActivityManager, CMPedometer, CMMotionManager -->
<key>NSMotionUsageDescription</key>
<string>App name uses motion data to detect whether you are walking or driving,
adapting navigation guidance to your current mode of transport.</string>

<!-- Location — required for CLLocationManager -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>App name uses your location while open to provide speed-aware activity
detection and accurate journey tracking.</string>

If you use requestAlwaysAuthorization() (for background detection):

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Background location access allows App name to automatically start and stop
journey logging without requiring the app to be open.</string>

What data is collected and how to explain it to users

Apple's App Privacy framework requires you to declare in App Store Connect:

  • Precise location: Yes, if you use kCLLocationAccuracyBest
  • Location (not precise): Yes, if you use coarser accuracy
  • Motion & Fitness: Yes, if you use CMMotionActivityManager or CMPedometer
  • Usage data: Depends on whether you log activity to your own servers

Best practice: Do not send raw sensor data to your servers. Process everything on-device. Only transmit derived labels (e.g., "trip from A to B, 15 minutes driving") rather than raw GPS tracks or accelerometer data.

App Store review considerations

Apple's reviewer guidelines (4.1, 5.1.1) require that location and motion use be "directly relevant to the core functionality" of the app. Activity detection to switch navigation modes is clearly justified. Using motion data purely for advertising targeting would not be approved.

The NSMotionUsageDescription string is shown to the user in the system permission prompt. Make it clear and honest — vague strings like "Used for app functionality" will result in App Store rejection.


13. Testing Strategy

Unit testing the fusion classifier

The fuseSignals function is a pure function (no side effects, no I/O) and is easily unit-tested:

import XCTest
@testable import YourApp

class ActivityDetectorTests: XCTestCase {
    
    var detector: ActivityDetector!
    
    override func setUp() {
        detector = ActivityDetector()
    }
    
    func testHighwayDrivingDetected() {
        let (type, confidence) = detector.fuseSignals(
            speed: 30.0,      // 108 km/h
            stepFrequency: 0.0,
            accelVariance: 0.008
        )
        XCTAssertEqual(type, .driving)
        XCTAssertEqual(confidence, "high")
    }
    
    func testNormalWalkingDetected() {
        let (type, confidence) = detector.fuseSignals(
            speed: 1.5,
            stepFrequency: 1.7,
            accelVariance: 0.045
        )
        XCTAssertEqual(type, .walking)
        XCTAssertEqual(confidence, "high")
    }
    
    func testTrafficJamIsAmbiguous() {
        let (_, confidence) = detector.fuseSignals(
            speed: 0.8,       // Could be walk or very slow drive
            stepFrequency: 0.0,
            accelVariance: 0.015
        )
        XCTAssertEqual(confidence, "low")
    }
    
    func testGPSUnavailableStillClassifiesWalking() {
        let (type, confidence) = detector.fuseSignals(
            speed: -1.0,      // GPS invalid
            stepFrequency: 1.6,
            accelVariance: 0.040
        )
        XCTAssertEqual(type, .walking)
        XCTAssertEqual(confidence, "medium") // Only 2 signals agree, so medium not high
    }
    
    func testRunningDetected() {
        let (type, _) = detector.fuseSignals(
            speed: 4.0,
            stepFrequency: 3.1,
            accelVariance: 0.180
        )
        XCTAssertEqual(type, .running)
    }
}

Integration testing on a real device

Simulators cannot simulate motion sensor data in a meaningful way for activity detection. All integration testing must be done on physical iPhones. Recommended test procedure:

  1. Walking test: Walk around a building exterior for 2 minutes; verify consistent walking detection with high confidence.
  2. Driving test: Drive on a suburban road for 5 minutes including stops at traffic lights; verify driving detection survives the stops.
  3. Transition test: Walk to a car, get in, start driving; measure transition detection latency.
  4. GPS-denied test: Walk through a shopping centre or car park; verify fallback to motion-only classification.
  5. Pocket vs hand test: Repeat walking test with phone in various positions; verify orientation invariance.

Using simulated GPS data for repeatable tests

For CI pipelines, you can inject GPX routes as simulated location data in the iOS Simulator:

Xcode → Debug → Simulate Location → Add GPX File to Project

Or inject programmatically using XCTest:

// In a UI test target
let app = XCUIApplication()
app.launchArguments.append("-simulateLocation")
app.launchArguments.append("walkingRoute.gpx")

14. Broader Perspectives: How Other Platforms Approach This

Android — Activity Recognition API

Google's Activity Recognition API (part of Google Play Services) offers a similar high-level interface:

val client = ActivityRecognition.getClient(context)
val intent = Intent(context, ActivityTransitionReceiver::class.java)
val transitions = listOf(
    ActivityTransition.Builder()
        .setActivityType(DetectedActivity.WALKING)
        .setActivityTransitionType(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
        .build()
)
val request = ActivityTransitionRequest(transitions)
client.requestActivityTransitionUpdates(request, pendingIntent)

Google classifies into: IN_VEHICLE, ON_BICYCLE, ON_FOOT, RUNNING, WALKING, STILL, TILTING, UNKNOWN. Like Apple, Google uses a combination of accelerometer, gyroscope, and GPS.

Key difference: Google's classifier runs partly server-side on older Android devices (to compensate for less capable hardware), introducing a network dependency that Apple avoids by running everything on the M-series coprocessor.

Wear OS / Apple Watch

Apple Watch adds significant discriminating capability:

  • Wrist accelerometer directly measures arm swing (a key walking signal) rather than relying on in-pocket/in-bag phone motion.
  • Heart rate sensor distinguishes exercise walking (elevated HR) from casual walking.
  • Optical heart rate correlation with step frequency provides a cleaner cadence signal than the phone accelerometer alone.
  • Gyroscope can distinguish walking arm swing from car passenger hand-arm motion.

The WKInterfaceController and CMMotionManager on watchOS follow the same APIs as iOS, with the benefit of more reliable placement (wrist vs arbitrary body location).

Dedicated activity trackers (Garmin, Fitbit)

Purpose-built fitness trackers solve the placement problem by guaranteeing wrist placement. They use simpler algorithms (step counting + threshold on cadence and GPS speed) because the accelerometer position is consistent. Without GPS (Fitbit Charge), pure accelerometer step counting has ~95% accuracy for walking vs stationary.

Research approaches: beyond rule-based classifiers

Academic literature (2015–2024) has explored several techniques beyond the rule-based approach described here:

Deep learning on raw sensor data. Convolutional neural networks applied directly to 2-second windows of raw accelerometer data (at 50 Hz) achieve 97–99% accuracy on the standard HAPT (Human Activity and Postural Transitions) benchmark dataset. The key advantage is automatic feature extraction — the network learns what features matter from data, eliminating hand-tuned thresholds.

Transformer-based models. Self-attention mechanisms applied to IMU time series outperform LSTMs on ambiguous transitions (slow run vs fast walk) by capturing long-range temporal dependencies.

Federated learning. Training activity classifiers across millions of devices without centralising sensitive sensor data. Apple's Core ML and on-device training infrastructure supports this pattern.

Multi-modal fusion with neural networks. Rather than rule-based voting, a fusion network takes all sensor streams as input and learns the optimal fusion weights end-to-end. This handles correlated signals and non-linear interactions that rule-based systems miss.


15. References and Further Reading

Apple Developer Documentation

  • CMMotionActivityManager — https://developer.apple.com/documentation/coremotion/cmmotionactivitymanager
  • CMPedometer — https://developer.apple.com/documentation/coremotion/cmpedometer
  • CMMotionManager — https://developer.apple.com/documentation/coremotion/cmmotionmanager
  • CLLocationManager — https://developer.apple.com/documentation/corelocation/cllocationmanager
  • CLLocation.speed and speedAccuracy — https://developer.apple.com/documentation/corelocation/cllocation/speed
  • Create ML Activity Classifier — https://developer.apple.com/documentation/createml/creating_an_activity_classifier_model

WWDC Sessions

  • WWDC 2014 Session 612 — "Core Motion" — Introduced CMMotionActivityManager and the M7 coprocessor
  • WWDC 2019 Session 705 — "Advances in Core Motion and Motion Tracking"
  • WWDC 2021 Session 10054 — "Create ML for activity classification"
  • WWDC 2023 Session 10044 — "Explore improvements in CoreMotion"

Academic Papers

  • Kwapisz, J.R., Weiss, G.M., & Moore, S.A. (2011). "Activity recognition using cell phone accelerometers." ACM SIGKDD Explorations. — One of the foundational papers on smartphone-based activity recognition.
  • Anjum, A., & Ilyas, M.U. (2013). "Activity recognition using smartphone sensors." IEEE CCNC. — Established the threshold ranges for walking vs driving that inform implementations like this one.
  • Chen, Z., et al. (2021). "Deep learning for sensor-based activity recognition." Information Fusion. — Survey of neural network approaches.
  • Demrozi, F., et al. (2020). "Human activity recognition using inertial, physiological and environmental sensors." IEEE Access.

Open Datasets for Training and Benchmarking

  • WISDM (Wireless Sensor Data Mining Lab) — labelled accelerometer data for walking, jogging, sitting, standing, upstairs, downstairs. http://www.cis.fordham.edu/wisdm/dataset.php
  • UCI HAPT (Human Activity and Postural Transitions) — smartphone sensor data at 50 Hz with 12 activity labels including walking, walking upstairs, sitting, standing. UCI Machine Learning Repository.
  • ExtraSensory — 300,000+ minutes of labelled data from real-world free-living settings. http://extrasensory.ucsd.edu
  • HKWorkoutSession — HealthKit workout tracking integrated with activity detection
  • CMFallDetectionManager — fall detection using activity context (requires Apple Watch Ultra/Series 4+)
  • CLBeaconRegion — indoor positioning using Bluetooth beacons (complements GPS-denied detection)
  • WKExtendedRuntimeSession — watchOS background session for continuous activity monitoring

Document version 1.0 — Last updated June 2026 Swift version: 5.9+ | iOS deployment target: iOS 16+ | Xcode 15+

autocmds

Source: autocmds · updated 2026-06-11 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

-- Tabline that shows ws_name tab-local variable when set, else falls back to
-- the buffer name. Used by LoadWorkspace to label tabs by folder name.
_G.WsTabline = function()
  local s = ""
  local ntabs = vim.fn.tabpagenr("$")
  for i = 1, ntabs do
    local label = vim.fn.gettabvar(i, "ws_name", "")
    if label == "" then
      local bufs = vim.fn.tabpagebuflist(i)
      label = vim.fn.fnamemodify(vim.fn.bufname(bufs[1]), ":t")
      if label == "" then label = "[No Name]" end
    end
    if i == vim.fn.tabpagenr() then
      s = s .. "%#TabLineSel# " .. i .. ":" .. label .. " "
    else
      s = s .. "%#TabLine# " .. i .. ":" .. label .. " "
    end
  end
  return s .. "%#TabLineFill#"
end
vim.opt.tabline = "%!v:lua.WsTabline()"
vim.opt.showtabline = 2

-- Cycle tabs with Alt-l / Alt-h
vim.keymap.set("n", "<A-l>", "<cmd>tabnext<cr>", { desc = "Next tab" })
vim.keymap.set("n", "<A-h>", "<cmd>tabprev<cr>", { desc = "Prev tab" })

-- Fuzzy-pick a tab with <leader>T (Alt+t unreliable on macOS)
vim.keymap.set("n", "<leader>T", function()
  local tabs = {}
  for i = 1, vim.fn.tabpagenr("$") do
    local name = vim.fn.gettabvar(i, "ws_name", "")
    if name == "" then
      local bufs = vim.fn.tabpagebuflist(i)
      name = vim.fn.fnamemodify(vim.fn.bufname(bufs[1]), ":t")
      if name == "" then name = "[No Name]" end
    end
    table.insert(tabs, { text = i .. ": " .. name, idx = i })
  end
  Snacks.picker.pick({
    title = "Tabs",
    items = tabs,
    format = function(item) return { { item.text } } end,
    confirm = function(picker, item)
      picker:close()
      if item then vim.cmd("tabn " .. item.idx) end
    end,
  })
end, { desc = "Pick tab" })

local function open_vscode_workspace(workspace_file)
  local f = io.open(workspace_file, "r")
  if not f then
    vim.notify("Could not open workspace file: " .. workspace_file, vim.log.levels.ERROR)
    return
  end
  local content = f:read("*a")
  f:close()

  local ok, ws = pcall(vim.json.decode, content)
  if not ok or not ws.folders then
    vim.notify("Failed to parse workspace file", vim.log.levels.ERROR)
    return
  end

  local ws_dir = vim.fn.fnamemodify(workspace_file, ":p:h")
  local paths = {}

  for _, folder in ipairs(ws.folders) do
    local path = folder.path
    if not vim.startswith(path, "/") then
      path = vim.fn.simplify(ws_dir .. "/" .. path)
    end
    if vim.fn.isdirectory(path) == 0 then
      vim.notify("Skipping missing directory: " .. path, vim.log.levels.WARN)
    else
      table.insert(paths, path)
    end
  end

  if #paths == 0 then return end

  for i, path in ipairs(paths) do
    local name = vim.fn.fnamemodify(path, ":t")
    if i == 1 then
      vim.cmd("tcd " .. vim.fn.fnameescape(path))
    else
      vim.cmd("tabnew")
      vim.cmd("tcd " .. vim.fn.fnameescape(path))
    end
    vim.t.ws_name = name
  end

  -- Open tab 1's explorer immediately; lazily open the rest on first TabEnter.
  -- Opening multiple Snacks pickers in the same event loop causes BufWinEnter
  -- nesting (E218) due to snacks' internal nvim_win_set_buf autocmd cascade.
  vim.cmd("tabfirst")

  local pending = {}
  for i = 2, #paths do
    pending[i] = paths[i]
  end

  local aug = vim.api.nvim_create_augroup("LoadWorkspaceExplorers", { clear = true })
  vim.api.nvim_create_autocmd("TabEnter", {
    group = aug,
    callback = function()
      local tabnr = vim.fn.tabpagenr()
      if pending[tabnr] then
        local p = pending[tabnr]
        pending[tabnr] = nil
        vim.schedule(function()
          Snacks.explorer.open({ cwd = p })
        end)
        -- Clean up autocmd once all tabs have been visited
        if vim.tbl_isempty(pending) then
          vim.api.nvim_del_augroup_by_id(aug)
        end
      end
    end,
  })

  -- Open the first tab's explorer last so it ends up focused
  vim.schedule(function()
    Snacks.explorer.open({ cwd = paths[1] })
  end)
end

vim.api.nvim_create_user_command("LoadWorkspace", function(opts)
  open_vscode_workspace(opts.args)
end, { nargs = 1, complete = "file" })

Source: skull · skull.md · updated 2026-06-03 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

I want you to create a thorough product and technical plan for a Go-based command-line tool that manages AI-agent “skills” hosted in GitHub repositories.

The tool should behave like a terminal-native extension manager for AI coding tools, similar in spirit to VS Code extensions, but focused only on GitHub-hosted skill directories.

Core goal: Build a single Go executable that allows users to register GitHub skill sources, discover valid skills from those sources, browse/search them, install them, validate them, detect updates, preview changes, update them, remove them, and use shell autocomplete and a polished TUI.

Important constraint: The user should install only one executable binary. The tool must not require users to install Git, SQLite, Node, Python, unzip, jq, or any external service. Any local storage must be embedded and created automatically by the executable.

Storage requirement: Do not use plain JSON as the primary internal store. Plan for a solid embedded local storage system suitable for many sources, many skills, autocomplete caching, discovery results, installed state, update status, file hashes, and TUI filtering. Prefer embedded SQLite unless there is a stronger reason to choose another embedded store. JSON may still be used for portable exports, lockfiles, or project-level configuration, but not as the main internal database.

GitHub-only scope: The tool should support GitHub only. It should not attempt to support GitLab, Bitbucket, generic Git, or marketplace payments in the initial design.

Discovery model: Do not scan all of GitHub. Do not scan entire repositories. Discovery must be explicit and bounded.

A user should register one or more GitHub skill sources. Each source should include:

  • source name
  • GitHub repo, for example owner/repo
  • ref, branch, tag, or commit
  • exact root path where skills are expected to live, for example skills/ or approved/skills/

Example: skill source add agensi github:agensi/skills@main:skills

The tool should discover only under configured source paths.

A single repo may have multiple source paths, for example:

  • github:agensi/skills@main:skills
  • github:agensi/skills@main:experimental
  • github:company/ai-skills@main:approved

Discovery should support:

  • source add/list/edit/remove/enable/disable/test
  • discover all enabled sources
  • refresh catalog and update status
  • autocomplete based on discovered catalog
  • TUI browsing based on discovered catalog

Discovery validation: The tool must verify that a configured source path is actually a skills root, not just a random GitHub directory.

A source path should be considered valid only if it contains a recognized skill index file or contains valid skill directories according to strict rules.

Preferred model: A source root may contain a skills index file, for example skills.index.json, listing skill directories and metadata.

Fallback model: If no index exists, discovery may inspect direct child directories only, or a carefully bounded max depth. It must not recursively scan unlimited directories. It should detect valid skill directories by the presence of required files.

Skill validation: A valid skill directory should include at minimum:

  • skill.json manifest
  • SKILL.md entry file

The tool must validate both during discovery and again before install/update.

Validation should reject or warn on:

  • missing skill.json
  • missing SKILL.md
  • invalid manifest schema
  • manifest name mismatch
  • invalid or duplicate skill names
  • paths outside the configured source root
  • path traversal such as ../
  • absolute paths
  • symlinks unless explicitly allowed
  • Git submodules unless explicitly allowed
  • hidden/sensitive files such as .env, *.pem, *.key
  • unexpected files not included by the manifest allowlist
  • huge files over configured size limits
  • binary files unless explicitly allowed
  • directories that are near the source path but are not actually skills

The plan should include how invalid directories are reported to the user during discovery without breaking discovery for the whole source.

Installation model: The installer should download only the selected skill files from GitHub. It should not clone the whole repo. It should use GitHub APIs to fetch the tree and blobs/content for only the relevant paths.

Before installing, the tool should show an install preview:

  • skill name
  • source
  • resolved commit
  • target
  • install path
  • files to install
  • warnings
  • validation result

Install should be atomic:

  • download to temporary location
  • validate content
  • backup existing installation
  • swap into final target directory
  • write local metadata
  • support rollback

Target model: The tool should support installing skills into different AI-agent targets, for example:

  • Claude Code
  • OpenClaw
  • Codex CLI
  • Cursor, if applicable

The plan should define a clean target abstraction, because a skill may be installed for one target and not another.

Revision and update model: Do not rely on GitHub Releases. Do not assume skills have formal versions. Skills may only be Markdown files, and a single text change can materially change behavior.

The tool should track:

  • optional human version from skill.json
  • GitHub ref being tracked, for example main
  • resolved installed commit
  • latest resolved commit
  • content hash of the actual skill files
  • file-level hashes
  • changed files

Update detection should be based primarily on content changes in the actual skill files, not only on repo commit changes and not only on manifest version changes.

The tool should clearly show:

  • current version and latest version if available
  • current revision and latest revision
  • whether content changed
  • which files changed
  • whether version changed
  • whether the skill is pinned
  • whether the upstream source disappeared
  • whether local files were modified

The user must be able to run:

  • skill check
  • skill outdated
  • skill diff
  • skill update
  • skill update --all
  • skill pin
  • skill rollback

Updates should never be applied silently by default. The user should see what changed before applying, especially for Markdown content changes.

Autocomplete: The CLI should provide excellent shell autocomplete for Bash, Zsh, Fish, and PowerShell.

Autocomplete must not call GitHub directly. It should query the local embedded database/cache only.

Autocomplete should support:

  • install: discovered but not installed skills
  • update: installed skills with updates available
  • remove: installed skills
  • source commands: configured source names
  • target names
  • tags or filters where useful

TUI: The tool should include a polished terminal UI written in Go.

The TUI should include screens such as:

  • Store / Catalog
  • Installed
  • Updates
  • Sources
  • Skill details
  • Diff / change preview
  • Doctor / health check

The TUI should allow:

  • search
  • filtering by source, target, installed status, update status, tags
  • install/update/remove actions
  • viewing validation warnings
  • viewing changed files
  • refreshing sources
  • adding/editing sources

CLI and CI: The tool should also work well in non-interactive mode.

Support:

  • --json output for machine-readable commands
  • --ci mode
  • deterministic exit codes
  • no prompts in CI mode
  • sync from project lockfile
  • check for drift or updates
  • project-level source and lock files for teams

Project-level files: The plan should include optional project files such as:

  • skills.sources.json
  • skills.lock

These files should be portable and reviewable, but the main internal local store should still be embedded storage.

Doctor/repair: The tool should include a doctor command that can detect and optionally fix:

  • missing target directories
  • broken installed metadata
  • stale cache
  • missing source paths
  • disabled or unreachable sources
  • invalid installed skills
  • local modifications
  • missing upstream skills
  • autocomplete cache issues

Security and trust: The plan should include safety and trust rules:

  • no arbitrary script execution during install
  • no implicit executable file installation unless allowed
  • clear validation before install/update
  • content hashes
  • provenance metadata for every installed skill
  • backup and rollback
  • transparent diff before update
  • support for GitHub token via env var or stored login
  • no GitHub calls during autocomplete

Expected output: Produce a complete product and technical plan, but do not write implementation code.

Structure the answer with:

  1. Product summary
  2. Primary workflows
  3. Source and discovery model
  4. Skill validation model
  5. Storage architecture
  6. Update/revision tracking model
  7. CLI command design
  8. TUI design
  9. Autocomplete design
  10. Install/update/remove lifecycle
  11. CI and lockfile behavior
  12. Data model at a conceptual level
  13. Failure modes and how the tool should report them
  14. MVP scope
  15. Future enhancements
  16. Open design questions

Keep the plan practical, implementation-oriented, and opinionated. Avoid vague marketplace language. The product should do one thing very well: manage GitHub-hosted AI skills from explicitly configured GitHub source paths.

tez

Source: tez · updated 2026-06-03 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

prompt.txt

# Prompt: Convert Academic Paper / PDF to Clean Readable Text

## Purpose
Use this prompt to extract and clean the full body text from an academic paper or any structured PDF document, producing a plain-text file optimized for reading or text-to-speech (TTS) playback.

---

## The Prompt

I have attached a PDF of an academic paper [or: a structured document]. Please extract and convert its full content into a clean plain-text (.txt) file suitable for reading or text-to-speech playback.

Follow these instructions precisely:

WHAT TO INCLUDE

  • The paper title (at the top)
  • All section headings and sub-headings, preserved exactly as written
  • All body text, in the correct reading order, flowing naturally as prose
  • Numbered or bulleted lists from the body text, reformatted as clean plain text
  • Footnotes or endnotes that contain substantive content (not just citations)
  • The Acknowledgements section, if present

WHAT TO REMOVE — completely omit the following:

  • Author names, institutional affiliations, and email addresses
  • The abstract label is fine to keep, but remove everything else on the title page that is not the title or abstract text
  • All copyright notices, license text, and permission statements
  • Conference or journal metadata (venue name, date, location, proceedings info)
  • DOI links, ACM/ISBN numbers, and any other publication identifiers
  • All page numbers (whether inline, in headers, or footers)
  • All figure captions (lines starting with "Figure N:" or "Fig. N:")
  • All table captions (lines starting with "Table N:")
  • All inline citation brackets (e.g. [1], [24], [3, 14]) — strip them silently from the text
  • The entire References / Bibliography section (the last section listing all cited works)
  • Any diagram labels, ASCII-art figures, or layout artifacts from multi-column formatting — these appear as isolated words or short tokens that belong to a figure or diagram, not to the prose, and have no meaning out of context
  • Running headers or footers that repeat the paper title or section name on every page

FORMATTING RULES

  • Output pure plain text — no Markdown, no HTML, no bold, no italics
  • Separate major sections with a single blank line above the section heading
  • Use two blank lines between major top-level sections
  • Preserve paragraph breaks as a single blank line
  • Re-join hyphenated words that were split across a line break (e.g. "architec-\nture" becomes "architecture")
  • For two-column PDF layouts: reconstruct the correct reading order — finish the left column completely before starting the right column, per page
  • Do not add any commentary, preamble, or summary — output only the cleaned document text

OUTPUT Deliver the result as a downloadable .txt file named after the paper title (use underscores for spaces).


---

## Notes on Usage

- **Multi-column PDFs** are the trickiest case. If the output looks scrambled (left and right columns interleaved), explicitly add to the prompt: *"This PDF uses a two-column academic layout. Reconstruct each page left-column-first, then right-column, before moving to the next page."*

- **Scanned PDFs** (no text layer) require OCR first. Add: *"This PDF appears to be a scanned image. Please use OCR to extract the text before cleaning."*

- **If citations in-text are important to keep** (e.g. you want to know which claims are cited), replace the "remove inline citation brackets" instruction with: *"Keep inline citations but rewrite them as readable callouts, e.g. [24] → (ref 24)."*

- **For books or long reports** with many chapters, add: *"Process and output one chapter at a time, clearly labeled, so the output stays manageable."*

- **For non-English papers**, add: *"The paper is written in [language]. Preserve the original language exactly — do not translate."*

tez.txt

Apache Tez: A Unifying Framework for Modeling and Building Data Processing Applications


ABSTRACT

The broad success of Hadoop has led to a fast-evolving and diverse ecosystem of application engines that are building upon the YARN resource management layer. The open-source implementation of MapReduce is being slowly replaced by a collection of engines dedicated to specific verticals. This has led to growing fragmentation and repeated efforts—with each new vertical engine re-implementing fundamental features (e.g. fault-tolerance, security, stragglers mitigation, etc.) from scratch.

In this paper, we introduce Apache Tez, an open-source framework designed to build data-flow driven processing runtimes. Tez provides a scaffolding and library components that can be used to quickly build scalable and efficient data-flow centric engines. Central to our design is fostering component re-use, without hindering customizability of the performance-critical data plane. This is in fact the key differentiator with respect to the previous generation of systems (e.g. Dryad, MapReduce) and even emerging ones (e.g. Spark), that provided and mandated a fixed data plane implementation. Furthermore, Tez provides native support to build runtime optimizations, such as dynamic partition pruning for Hive.

Tez is deployed at Yahoo!, Microsoft Azure, LinkedIn and numerous Hortonworks customer sites, and a growing number of engines are being integrated with it. This confirms our intuition that most of the popular vertical engines can leverage a core set of building blocks. We complement qualitative accounts of real-world adoption with quantitative experimental evidence that Tez-based implementations of Hive, Pig, Spark, and Cascading on YARN outperform their original YARN implementation on popular benchmarks (TPC-DS, TPC-H) and production workloads.


1. INTRODUCTION

Large scale data analytics, once an exotic technology leveraged exclusively by large web-companies, is nowadays available and indispensable for most modern organizations. This broader user base has fostered an explosion of interest in this area, and led to a flourishing BigData industry. In this paper, we use the lens of the Hadoop ecosystem to describe industry-wide trends, as this provides the ideal context for introducing our system: Apache Tez.

We postpone to Section 8 a broader comparison with the related projects like Dryad, Nephele, Hyracks etc., which undeniably served as an inspiration and sometimes the blueprint for the design of Tez.

Hadoop, which was initially designed as a single-purpose system (to run MapReduce jobs to build a web index), has evolved into a catch-all data analytics platform. The first phase of this journey consisted of several efforts proposing higher level abstractions atop MapReduce, examples of which are Hive, Pig, and Cascading. This sped-up the adoption of Hadoop, but led to inefficiencies and poor performance. These limitations and the pressure towards more flexibility and efficiency led to the refactoring of Hadoop into a general purpose, OS-like resource management layer, namely YARN, and an application framework layer allowing for arbitrary execution engines. This enabled different applications to share a cluster, and made MapReduce just another application in the Hadoop ecosystem. Important examples of applications that break-free of the MapReduce model (and runtime) are Spark, Impala and Flink. This has accelerated innovation, but also led to a less efficient ecosystem, where common functionalities were being replicated across frameworks. For example, MapReduce and Spark independently developed mechanisms to implement delay scheduling.

In this paper, we introduce Tez, a project that embraces the architectural shift to YARN, and pushes it further, by proposing a reusable, flexible and extensible scaffolding that can support arbitrary data-flow oriented frameworks, while avoiding replicated functionalities. Tez APIs allow frameworks to clearly model the logical and physical semantics of their data flow graphs, with minimal code. It is important to clarify that Tez is a library to build data-flow based runtimes/engines and not an engine by itself—for example, the Hive runtime engine for Hadoop has been rewritten in version 0.13 to use Tez libraries.

Tez makes the following key contributions:

1. Allows users to model computation as a DAG (Directed-Acyclic-Graph) — akin to Dryad/Nephele/Hyracks. The novelty lies in a finer grained decomposition of the classical notions of vertex and edge, that delivers greater control and customization of the data plane.

2. Exposes APIs to dynamically evolve the (finer grained) DAG definition. This enables sophisticated runtime query optimizations, such as pruning data partitions, based on online information.

3. Provides a scalable and efficient implementation of state-of-the-art features, e.g., YARN-compatible security, data-locality awareness, resource-reuse, fault-tolerance and speculation.

4. Provides the opportunity for framework writers and researchers to innovate quickly and create real-world impact by providing experimentation support via pluggable APIs, and an open-source community to learn about the project and contribute back to it.

What sets Tez aside from many alternative proposals of 'unification frameworks' is: 1) proven flexibility and dynamic adaptation, 2) attention to operational concerns (production readiness), and 3) a community-driven effort to embed Tez in multiple existing domain-specific engines.

This is proven by the Tez support of MapReduce, Hive, Pig, Spark, Flink, Cascading, and Scalding, and its adoption in production data-processing clusters at Yahoo, Microsoft Azure, LinkedIn as well as several other organizations using the Hortonworks Data Platform. Beyond discussing the broad practical adoption of Tez, we demonstrate its competence to support Hive, Pig, and Spark, by running standard benchmarks such as TPC-H and TPC-DS, and production workloads from Yahoo!.

The rest of this paper is organized as follows: Section 2 provides some more historical context, and rationale for the design of Tez, while Section 3 introduces the architecture of Tez. Section 4 discusses the implementation of Tez, and highlights pragmatic considerations on efficiency and production-readiness. Section 5 and 6 are devoted to prove its practical relevance, by presenting real-world applications, and a broad experimental evaluation. We conclude by discussing future and related work in Sections 7 and 8, and conclude in Section 9.


2. BACKGROUND AND RATIONALE

To understand the motivation and rationale behind Tez, we must first start by providing some background on terminology, and a historical context of distributed computation in Hadoop. The reader not interested in this historical and motivational perspective is invited to continue to Section 3, where we dive into the technical aspects of the Tez architecture.

Terminology. We have used graph terminology so far, appealing to the reader's intuitions. We now introduce our terminology more precisely:

DAG: Directed Acyclic Graph representing the structure of a data processing workflow. Data flows in the direction of the edges.

Vertex: Represents a logical step of processing. A processing step transforms data by applying application-supplied code to filter, or modify the data.

Logical DAG: A logical DAG is comprised of a set of vertices, where each vertex represents a specific step of the computation.

Task: Represents a unit of work in a vertex. In distributed processing, the logical work represented by a single vertex is physically executed as a set of tasks running on potentially multiple machines of the cluster. Each task is an instantiation of the vertex, that processes a subset (or partition) of the input data for that vertex.

Physical DAG: A physical DAG comprises of the set of tasks that are produced by expanding the vertices of a logical DAG into their constituent tasks.

Edge: Represents movement of data between producers and consumers. An edge between vertices of a logical DAG represents the logical data dependency between them. An edge between tasks in the physical DAG represents data transfers between the tasks.

This applies to problems in which different steps can be partitioned into smaller pieces that can be processed in parallel. Typically, the partitioning aligns with distributed shards of data and tries to co-locate processing with its data, thus reducing the cost of computation.

Hadoop 1 and Hadoop 2 (YARN). Hadoop started off as a single monolithic software stack where MapReduce was the only execution engine. All manners of data processing had to translate their logic into a single MapReduce job or a series of MapReduce jobs. MapReduce was also responsible for cluster resource management and resource allocation. Hadoop 2 is the current generation of Hadoop which separates these responsibilities by creating a general purpose resource management layer named YARN. This de-couples applications from the core Hadoop platform and allows multiple application types to execute in a Hadoop cluster in addition to MapReduce. There are many domain-specific applications like Apache Hive for SQL-like data processing, Apache Pig for ETL scripting or Cascading for writing data processing applications in Java, which were earlier restricted to relying on MapReduce to execute their custom logic. These applications can now have a more customized implementation of their logic by running natively on YARN.

While specialization can deliver performance advantages, there is a substantial opportunity to create a common set of building blocks that can be used by these applications for their customized implementation on YARN. We try to seize that opportunity with Tez, as discussed next. An analysis of popular Hadoop ecosystem applications like Apache Hive, Pig, Spark etc. suggests that there are shared features that all of them need. These include negotiating resources from YARN to run application's tasks, handling security within the clusters, recovering from hardware failures, publishing metrics and statistics etc. A lot of this is highly specialized, hard to develop infrastructure that everyone has to replicate when building from scratch. A common implementation makes it easier to write applications because it removes that burden from the application writers and lets them focus on the unique logic of their application.

Henceforth, unless otherwise specified, when we mention Hadoop we imply the Hadoop 2 compute stack with YARN as the underlying resource allocation layer; and by Hadoop ecosystem we imply the compute ecosystem consisting of open source and commercial projects running on YARN such as Apache Hive, Pig etc.

An effort to provide these common features requires the creation of a framework to express and model these workloads optimally. Then this model can be applied and executed on the YARN application framework via a shared substrate library. This rationalizes the following requirements for such a shared library, which we have highlighted by comparisons with MapReduce — a general purpose engine that has been forced to act as shared substrate until now.

Expressiveness. MapReduce has a simple modeling API for describing the computation by requiring all application algorithms to be translated into map and reduce functions. As observed by others before, this is too constraining, and a DAG-oriented model can more naturally capture a broader set of computations. Thus we define Tez's central model around DAGs of execution as well. Moreover, MapReduce also provides built-in semantics to the logic running in map/reduce steps and imposed a sorted & partitioned movement of data between map and reduce steps. These built-in semantics, ideal in some core use cases, could be pure overhead in many other scenarios and even undesirable in some. The observation here is the need for an API to describe the structure of arbitrary DAGs without adding unrelated semantics to that DAG structure.

Data-plane Customizability. Once the structure of distributed computation has been defined, there can be a variety of alternative implementations of the actual logic that executes in that structure. These could be algorithmic, e.g. different ways of partitioning the data or these could be related to using different hardware, e.g. using remote memory access (RDMA) where available. In the context of MapReduce, the built-in semantics of the engine makes such customizations difficult because they intrude in the implementation of the engine itself. Secondly, the monolithic structure of the tasks executing the MapReduce job on the cluster makes plugging in alternative implementations difficult. This motivates that data transformations and data movements that define the data plane need to be completely customizable. There is a need to be able to model different aspects of task execution in a manner that allows individual aspects of the execution, e.g. reading input, processing data etc. to be customized easily. Interviewing several members of the Hadoop community we confirmed that evolving existing engines (e.g., changing the shuffle behavior in MapReduce) is far from trivial.

While other frameworks already support a more general notion of DAGs, they share the same limitation of MapReduce, built-in semantics and implementations of the data-plane. With Tez we provide a lower level abstraction, that enables such semantics and specialized implementations to be added on top of a basic shared scaffolding.

Late-binding Runtime Optimizations. Applications need to make late-binding decisions on their data processing logic for performance. The algorithm, e.g. join strategies and scan mechanisms, could change based on dynamically observing data being read. Partition cardinality and work division could change as the application gets a better understanding of its data and environment. Hadoop clusters can be very dynamic in their usage and load characteristics. Users and jobs enter and exit the cluster continuously and have varying resource utilization. This makes it important for an application to determine its execution characteristics based on the current state of the cluster. We designed Tez to make this late-binding and on-line decision-making easier to implement, by enabling updates to key abstractions at runtime.

This concludes our overview of historical context and rationale for building Tez. We now turn to describing the high level architecture of Tez, and provide some insight into the key building blocks.


3. ARCHITECTURE

Apache Tez is designed and implemented with a focus on the issues discussed above, in summary: 1) expressiveness of the underlying model, 2) customizability of the data plane, and 3) facilitate runtime optimizations. Instead of building a general purpose execution engine, we realize the need for Tez to provide a unifying framework for creating purpose-built engines that customize data processing for their specific needs. Tez solves the common, yet hard problem of orchestrating and running a distributed data processing application on Hadoop and enables the application to focus on providing specific semantics and optimizations. There is a clear separation of concerns between the application layer and the Tez library layer. Apache Tez provides cluster resource negotiation, fault tolerance, resource elasticity, security, built-in performance optimizations and a shared library of ready to use components. The application provides custom application logic, custom data plane and specialized optimizations.

This leads to three key benefits: 1) amortized development costs (Hive and Pig completely rewrote their engines using the Tez libraries in about 6 months), 2) improved performance (we show in Section 6 up to 10x performance improvement while using Tez), and 3) enabling future pipelines that leverage multiple engines, to be run more efficiently because of a shared substrate.

Tez is composed of a set of core APIs that define the data processing and an orchestration framework to launch that on the cluster. Applications are expected to implement these APIs to provide the execution context to the orchestration framework. Its useful to think of Tez as a library to create a scaffolding representing the structure of the data flow, into which the application injects its custom logic (say operators) and data transfer code (say reading from remote machine disks). This design is both tactical and strategic. Long-term, this makes Tez remain application agnostic while in the short term, allows existing applications like Hive or Pig to leverage Tez without significant changes in their core operator pipelines. We will begin with describing the DAG API and Runtime API. These are the primary application facing interfaces used to describe the DAG structure of the application and the code to be executed at runtime. Next we explain support for applying runtime optimizations to the DAG via an event based control plane using VertexManagers and DataSourceInitializers. Finally, in Section 4 we describe the YARN based orchestration framework to execute the all of this on a Hadoop cluster. In particular, we will focus on the performance and production-readiness aspects of the implementation.


3.1 DAG API

The Tez DAG API is exposed to runtime engine builders as an expressive way to capture the structure of their computation in a concise way. The class of data processing application we focus on, are naturally represented as DAGs, where data proceeds from data sources towards data sinks, while being transformed in intermediate vertices. Tez focuses on acyclic graphs, and by assuming deterministic computation on the vertex and data routing on the edges, we enable re-execution based fault tolerance. Modeling computations as a DAG is not new but hitherto most systems have typically designed DAG APIs in the context of supporting a higher level engine. Tez is designed to model this data flow graph as the main focus. Using well-known concepts of vertices and edges the DAG API enables a clear and concise description of the structure of the computation.

Vertex. A vertex in the DAG API represents transformation of data and is one of the steps in processing the data. This is where the core application logic gets applied to the data. Hence a vertex must be configured with a user-provided processor class that defines the logic to be executed in each task. One 'vertex' in the DAG is often executed in parallel across a (possibly massive) number of parallel tasks. The definition of a vertex controls such parallelism. Parallelism is usually determined by the need to process data that is distributed across machines or by the need to divide a large operation into smaller pieces. The task parallelism of a vertex may be defined statically during DAG definition but is typically determined dynamically at runtime.

Edge. An edge in the graph represents the logical and physical aspects of data movement between producer and consumer vertices.

Connection Pattern: The logical aspect of an edge is the connection pattern between producer and consumer vertex tasks and their scheduling dependency. This enables the orchestration framework to route data from the output of the producer task to the correct input of the consumer task. This routing table must be specified by implementing a pluggable EdgeManagerPlugin API. There are 3 common connection patterns (one-to-one, broadcast, scatter-gather), that can be used to express most DAG connections and come built-in with the project. For cases where custom routing is needed, applications are allowed to define their own routing by providing their own implementation.

Transport Mechanism: The physical aspect of an edge is the storage or transport mechanism employed to move the data. This could be local-disk, or local/remote main-memory, etc. The actual data transfer operation of the edge is performed by a compatible pair of input and output classes that are specified for the edge. Compatibility is based on using the same data format and physical transport mechanisms. E.g. both operate on key-value pairs and operate on disks, or both operate on byte streams and use main memory. Tez comes with built-in inputs and outputs for common use cases.

Vertex parallelism and the edge properties can be used by Tez to expand the logical DAG to the real physical task execution DAG during execution.

Data Sources and Sinks. The DAG can be defined by creating vertices and connecting them via edges using the DAG API. Typically, the data flow will read initial input from some data sources and write final output to some data sinks. Data sources may be associated with a DataSourceInitializer that can be invoked at runtime to determine the optimal reading pattern for the initial input. E.g. in MapReduce parlance, this corresponds to 'split' calculation where a split is a shard of distributed data that is read by a map task. The initial split calculation for map tasks can be performed using an initializer that considers the data distribution, data locality and available compute capacity to determine the number of splits and the optimal size of each split. Similarly, data sinks may be associated with a DataSinkCommitter that is invoked at runtime to commit the final output. The definition of commit may vary with the output type but is guaranteed to be done once, and typically involves making the output visible to external observers after successful completion.

This manner of DAG assembly allows for pluggable and re-usable components. A common shared library of inputs and outputs can be re-used by different applications, thus only needing to supply the processor logic in a vertex. Conversely, the same DAG structure may be executed more optimally in a different hardware environment by replacing the inputs/outputs on the edges. Tez comes with an input/output library for data services built into Hadoop - HDFS and the YARN Shuffle Service. This enables Hadoop eco-system applications like Hive and Pig to quickly leverage Tez by implementing only their custom processors.


3.2 Runtime API

The DAG API defines the scaffolding structure of the data processing. The Runtime API is used to inject the actual application code that fills the scaffolding. Concretely, the Runtime API defines the interfaces to be implemented to create processor, input and output classes that are specified in the DAG above.

Inputs, Processor, Outputs. A vertex is a logical representation of a transformation step in the DAG. The actual transformations are applied by running tasks, for that vertex, on machines in the cluster. Tez defines each task as a composition of a set of inputs, a processor and a set of outputs (IPO). The processor is defined by the vertex for that task. The inputs are defined by the output classes of the incoming edges to that vertex. The outputs by the input classes of the outgoing edges from that vertex. This enables the processor to have a logical view of the processing, thus retaining the simplified programming model popularized in MapReduce. The inputs and outputs hide details like the data transport, partitioning of data and/or aggregation of distributed shards. The Runtime API is a thin wrapper to instantiate and interact with inputs, processors and outputs. After the IPO objects have been created, they are configured.

IPO Configuration. The framework configures IPOs via an opaque binary payload specified during DAG creation. This manner of binary payload configuration is a common theme to configure any application specific entity in Tez. This allows applications to instantiate their code using any mechanism of their choice. Not only can this be used for simple configuration but also for code injection. After configuration, the processor is presented with all its inputs and outputs and asked to run. Thereafter, it's up to the processor, inputs and outputs to cooperate with each other to complete the task. The framework interacts with them via a context object to send and receive events about completion, update progress, report errors etc.

Data Plane Agnostic. Tez specifies no data format and in fact, is not part of the data plane during DAG execution. The actual data transfer is performed by the inputs and outputs with Tez only routing connection information between producers and consumers. When a producer task output generates data then it can send metadata about it, say its access URL and size, via Tez, to the consumer task input. Tez routes this metadata using the connection pattern encoded in the edge connecting the producer and consumer. Thus Tez adds minimal overhead on the data plane. This also makes Tez data format agnostic. The inputs, processor and outputs can choose their own data formats (e.g. bytes, records or key-value pairs etc.) as suited for the application.

This novel IPO based approach to task composition allows for separation of concerns and makes the system pluggable. The same DAG structure can be instantiated with environment dependent IOs. E.g. different cloud environments can plug in IOs that are optimized for their storage subsystems. We will see in the next sections, how the IPOs can be dynamically configured during execution for even further runtime customizations.


3.3 Event Based Control Plane

The open architecture of the Tez orchestration framework requires a de-coupled control plane that allows a variety of entities to communicate control information with each other. In order to achieve this Tez has an event based control plane that is also exposed to the application. Software components generate events that get routed to receivers. By design, this is an asynchronous, non-blocking, push-based method of communication. Events are used for all communications, be it framework to framework, application to framework and vice versa, or application to application.

A DataEvent is generated by the output of a producer task and contains output metadata (say a URL) for the consumer task to read the data. This event is received by the framework and routed to the input of the consumer task by utilizing the connection information specified by the edge. If a task input has an error while reading its data then it can send an ErrorEvent to the framework. Based on such error events, Tez could re-execute the producer task to re-generate the data. Other events could be used to send statistics, progress etc. Event based communication also provides the flexibility to add more entities and communication channels without changing the interaction model or APIs. Tez only routes the events. Each event has an opaque binary payload that is interpreted by the sender and receiver to exchange control metadata. Events flow to and from tasks to the orchestrator on every task heartbeat. Event transfer latency depends on the heartbeat latency and processing latency at the orchestrator. These latencies increase in proportion to the size of the job as they depend on the number of concurrent connections and event load supported by the orchestrator. If control plane events lie on the data plane critical path then they would negatively affect application latency but if they are used only for data plane setup then Tez would not introduce any additional latency on the data plane for low latency applications.


3.4 Vertex Manager: dynamically adapting the execution

As motivated earlier, data processing clusters have variability in compute capacity or data distribution (where data is stored on physical nodes) that applications may consider to plan their work. Data dependent actions like sample based range partitioning or optimizations like partition pruning need the ability to change the DAG on the fly. It is not possible to encode all such current and future graph re-configurations statically, nor can this be done by Tez itself (as it requires too much domain knowledge). Thus Tez needs to allow the application to make such decisions at runtime and coordinate with Tez to dynamically adapt the DAG and its execution. This is enabled via the VertexManager abstraction.

Runtime Graph Re-configuration. When constructing the DAG, each vertex can be associated with a VertexManager. The VertexManager is responsible for vertex re-configuration during DAG execution. The orchestration framework contains various state machines that control the life-cycle of vertices, tasks etc. and the vertex state machine is designed to interact with the VertexManager during state transitions. The VertexManager is provided a context object that notifies it about state changes like task completions etc. Using the context object, the VertexManager can make changes to its own vertex's state. Among other things, the VertexManager can control the vertex parallelism, the configuration payloads of the inputs, processors and outputs, the edge properties and scheduling of tasks. As with other entities there is a VertexManager API that can be implemented by applications to customize the vertex execution. Using the same API, Tez comes with some built-in VertexManagers. If a VertexManager is not specified in the DAG, then Tez will pick one of these built-in implementations based on the vertex characteristics.

Automatic Partition Cardinality Estimation. As an example of a runtime optimization, we present a solution to a well-known problem in MapReduce about determining the correct number of tasks in the reduce phase. This number typically depends on the size of the data being shuffled from the mappers to the reducers and is accurately available only at runtime. Shuffle is the term used to describe the cross-network read and aggregation of partitioned input done prior to invoking the reduce operation. In Tez, the ShuffleVertexManager can be used to control the vertices that are reading shuffled data. The tasks producing the data to be shuffled, send data statistics to the ShuffleVertexManager using VertexManager events. The ShuffleVertexManager gathers these statistics to calculate the total data size and estimate the correct number of reducers to read that data using a per-reducer desired data size heuristic. Since the number of reducers essentially represents the partition cardinality, this solution can be generalized to estimating the optimal number of partitions at runtime (e.g. partitions participating in a distributed join operation).

Scheduling Optimizations. VertexManagers also control the scheduling of tasks in their vertex. Typically, tasks should be started after their input data is ready. However, if the tasks can proceed meaningfully with partial input then they could be started out of order and use any free compute capacity. The shuffle operation mentioned above is an example of a case where partial inputs can be read by tasks pro-actively. This is an expensive data transfer across the network and starting early can help hide its latency by overlapping it with the completion of tasks that will produce the remaining input. Out of order scheduling can result in scheduling deadlocks in a resource constrained cluster where an out of order task ends up blocking one of its input tasks because it has occupied resources in the cluster. Tez has built-in deadlock detection and preemption to take care of such situations. It will use the DAG dependency to detect tasks running out of order and preempt them to resolve the deadlock.


3.5 Data Source Initializer

In Tez we have modeled data sources as first class entities in our design. The first step in a DAG usually involves reading initial input from data sources like distributed file systems, and typically is the largest in terms of resource consumption. Hence, a good or bad decision at this step can significantly improve or degrade performance. A data source in a DAG can be associated with a DataSourceInitializer that is invoked by the framework before running tasks for the vertex reading that data source. The initializer has the opportunity to use accurate information available at runtime to determine how to optimally read the input. Like the VertexManager, the initializer can also send and receive events from other entities. It also has access to cluster information via its framework context object. Based on these and other sources of information, the initializer can configure the task inputs or notify the vertex manager about vertex re-configurations (e.g. the optimal parallelism needed to process the input).

As an example, we will present a Hive dynamic partition pruning use case. It often happens that a data source will be read and subsequently joined on some key. If the join key space is known then we could only read a subset of the data that is relevant to the join. Sometimes this metadata is only available at runtime after inspecting the data in a different sub-graph of the DAG. Hive uses InputInitializer events to send this metadata from tasks in the other vertices to the initializer of the data source. The initializer uses that metadata to decide the relevant subset of data to read. This can lead to large performance gains depending on the join selectivity.

The above discussion has been a broad overview of the architecture and features in Tez. More details about the semantics around the APIs and user defined entities is available in the API documentation on the project website.


4. IMPLEMENTATION AND PRACTICAL CONSIDERATIONS

We now turn to describing how the architecture of the previous section is instantiated in YARN, and discuss in more details efficiency and production-readiness aspects of Tez. From an engineering perspective this is where much of our effort was devoted, and what makes Tez a useful building block for data-processing engines.


4.1 Implementation in YARN

The Apache Tez project consists of 3 main parts:

- API library: This provides the DAG and Runtime APIs and other client side libraries to build applications.

- Orchestration framework: This has been implemented as a YARN Application Master (hereafter referred to as AM) to execute the DAG in a Hadoop cluster via YARN.

- Runtime library: This provides implementations of various inputs and outputs that can be used out of the box.

Typical Tez Application Lifecycle. A Tez based application is written using the API library by constructing the DAG representing the application logic. Typically, higher level applications like Apache Pig construct DAGs on the fly by encoding their native language constructs into Tez DAGs. Since Tez is designed to operate in Hadoop clusters we have provided implementations of inputs and outputs to standard storage services present in all Hadoop clusters - HDFS for reliable data storage and YARN Shuffle Service for temporary data storage. Applications that use only these services, need to implement just their processors to get up and running. Typically applications create a generic processor host that can be configured to execute DAG dependent operators. Tez inputs and outputs are based on the key-value data format for ease of use within the key-value dominated Hadoop ecosystem of projects like Apache Hive, Pig etc., and can be extended to other data formats. The DAG is then submitted to a YARN cluster using the Tez client library. YARN launches the Tez Application Master (AM - a per-application controller) to orchestrate the DAG execution. The DAG executed by the AM is typically a logical DAG that describes the data flow graph. The AM expands this graph to incorporate task parallelism per vertex. It does this using the input initializers and vertex managers specified in the DAG. The AM then requests YARN for resources to run the tasks from different vertices. YARN responds in the form of containers. A container is a unit of resource allocation on a cluster node. The AM launches tasks on these containers and routes control events. Tasks are typically executed in their dependency order and the DAG completes when all its tasks complete. The AM logs tracing and metadata information for monitoring and debugging.

By leveraging existing libraries and services from YARN and MapReduce, we have been able to quickly build on top of several man-years of production ready code for security and high volume network data shuffling; and integrate with the proven resource sharing and multi-tenancy model in YARN. Thus, applications built using Tez will benefit from all these without expending further effort.


4.2 Execution Efficiency

The YARN implementation of the orchestration framework is built with execution efficiency and performance in mind and incorporates well known ideas learned over the years in various distributed data processing systems.

Locality Aware Scheduling. Scheduling processing close to the data location is important for large-scale data-processing. Tez tries to run tasks close to their input data location. Location may be specified statically during DAG creation but is typically determined at runtime. The tasks that read from initial input data sources typically get locality information from their data sources while intermediate task locality is inferred from their source tasks and edge connections. E.g. tasks with scatter-gather inputs have no specific locality but may prefer to run close to the larger input data shards. 1-1 edges specify strict locality relationships between their source and destination tasks. Since getting perfect locality may not be guaranteed in a busy cluster, the framework automatically relaxes locality from node to rack and so on with delay scheduling used to add a wait period before each relaxation.

Speculation. Large clusters can have heterogeneous hardware and varying loads and hardware-aging. This can lead to environment induced task slowdowns. Such slow tasks are termed stragglers and launching a clone of such tasks is typically used to mitigate their effects on latency. Tez monitors task progress and tries to detect straggler tasks that may be running much slower than other tasks in the same vertex. Upon detecting such a task, a speculative attempt may be launched that runs in parallel with the original task and races it to completion. If the speculative attempt finishes first then it is successful in improving the completion time.

Container Reuse. Recall that the Tez AM runs tasks in containers allocated to it by YARN. When a task completes, the AM has an option to return the container to YARN and ask for another container with different capabilities or locality. However, each container allocation cycle has overheads associated with resource negotiation from YARN as well as launching the container process. This overhead can be minimized by re-using the container to run other pending tasks that match the resource allocation and locality of that container. When there are no such matching tasks, the Tez AM releases the idle containers back to YARN in return for new resources with different capabilities. In the Java world, this reuse has the additional benefit of giving the JVM optimizer a longer time to observe and optimize the hot code paths leading to further performance benefits.

Session. A session takes the concept of container reuse one step further. A Tez AM can be run in session mode in which it can run a sequence of DAGs submitted to it by the client. This allows tasks from multiple DAGs to reuse containers and leads to further efficiencies and performance gains. In addition, a session can be pre-warmed by requesting the AM to launch containers before the first DAG is ready to execute. These pre-warmed containers can execute some pre-determined code to allow JVM optimizations to kick in. This extends the benefits of container reuse to the first DAG that gets submitted to the session. E.g. Apache Hive and Pig use the session mode to run multiple drill-down queries in the same session for performance benefits. Tez sessions also enable iterative processing to be performed efficiently. Each iteration can be represented as a new DAG and submitted to a shared session for efficient execution using pre-warmed session resources.

Shared Object Registry. Tez extends the benefits of container reuse to the application by providing an in-memory cache of objects that can be populated by a task and then re-used by subsequent tasks. The lifecycle of objects in the cache can be limited to a vertex, a DAG or the session and is managed by the framework. It can be used to avoid re-computing results when possible. E.g. Apache Hive populates the hash table for the smaller side of a map join in Hive parlance (broadcast join). Once a hash table has been constructed by a join task, other join tasks don't need to re-compute it and improve their performance.


4.3 Production Readiness

While performance and efficiency are important for a framework such as Tez, we cannot ignore the standard abilities that are prerequisites for a production ready and dependable framework for large scale data processing. A plethora of entities use technologies like Apache Hive, Pig and other commercial software like Cascading to run mission critical operations. If they are to confidently build using Apache Tez, then items like fault tolerance, security and multi-tenancy become necessary requirements. Fortunately, Tez has been able to build on top of proven and tested platforms like Hadoop YARN and MapReduce and draws from their strengths for achieving some of these abilities. The YARN integration exemplifies the specialized code implemented in Tez that can be leveraged by higher-level engines using Tez.

Multi-Tenancy. Data processing clusters are becoming increasingly large and sharing their capital expenditure among multiple applications and users is essential from a capex point of view. Applications must be written with such sharing and cooperative behavior in mind. The discrete task based processing model in Tez lends itself nicely to such cooperative behavior. Short lived ephemeral tasks allow resources to be periodically released by Tez applications so that they can be allocated to other users and applications as deemed appropriate by the cluster resource allocation policy. This also enables higher resource utilization by transferring resources from applications that don't need them to applications that do.

This is where engines that effectively deploy services daemons suffer from a drawback. Typically, the service daemons have to pre-allocate a large share of resources that cannot be shared with other applications. For better utilization, these daemons try to run multiple 'tasks' concurrently but that is not useful when there isn't enough load on the system, besides introducing the possibility of interference between concurrent tasks. With Tez, since each task runs in its own container process, the resource allocations are much finer grained. This improves utilization (by reducing allocated resources that are idle) and also provides process-based resource isolation (for CPU/memory etc.). This also provides resource elasticity to Tez applications in that they can scale up to utilize as much resources as the cluster can spare to speed up job execution time while gracefully degrading performance but still completing the job when resources are scarce. To be clear, this discussion about daemon based designs is in the specific context of ephemeral data processing jobs. There are many contexts like data storage, web services, PAAS applications etc. where a long running shared service provided by a daemon based engine is suitable.

Security. Security is real concern with the variety and volume of data stored in modern data processing clusters. Hadoop has built-in Kerberos and token based authentication and access control and Tez natively integrates with the Hadoop security framework to provide the application with secure access. In addition to that, the inputs and outputs provided with Tez support encryption for data read across the network. Security is a real concern with the variety of data stored and concurrent access from multiple users. Being outside the data plane reduces the contribution of Tez in the threat surface of the application. The only interaction between Tez and the app is control metadata routed via events by Tez. This metadata is presented to Tez as an opaque binary payload and thus can be protected by the app by encryption or other techniques as deemed necessary. In the control plane, secure Hadoop provides Kerberos and token based authentication for applications to access storage or compute resources and Tez integrates with the secure APIs exposed by Hadoop. Tez has some built-in input and output libraries for HDFS and local storage. In a secure Hadoop cluster, these libraries use HDFS token based authentication to access the data. In a secure cluster local data is written in the OS security content of the user and read via secure SSL channel provided by the YARN Shuffle Service.

Another aspect of security is isolation between tasks running on the same machine but belonging to different users. YARN provides this security isolation between containers by running the containers in the security context of the application user. Due to its fine-grained, ephemeral task model, Tez can leverage this container security by running tasks for single user in the containers of an application, thus guaranteeing user level isolation. This is much harder to achieve when using application engines that deploy service daemons. The daemons need to run tasks from different users in the same daemon process, making security isolation difficult or impossible. To work-around this, multiple instances of the service daemons need to be launched (one per user) and that may reduce resource utilization, as described above. We believe that the fine-grained, ephemeral task model of Tez makes it more suitable for secure and multi-tenant YARN clusters.

Fault Tolerance. Failures are a norm in clusters of commodity hardware. Failures can be on the compute nodes or the network. Tez provides robust fault tolerance against failures using task re-execution as a means of recovering from errors. When a task fails due to machine errors, it is re-executed on a different machine. Task re-execution based fault tolerance depends on deterministic and side-effect free task execution. Being side-effect free allows a task to be executed multiple times. Being deterministic, guarantees that if identical code is executed on identical input data then it will produce identical output data for each execution. These enable the system to safely re-execute tasks to recover from failures and data loss. Since the outputs are identical, the already completed consumer tasks of that output do not need to be re-executed. This limits the amount of re-execution and reduces the cost of failures.

Since Tez is not on the data plane, it exposes an InputReadError event that task inputs can use to notify Tez about loss of intermediate data. Using the DAG dependency information Tez can determine which task outputs produced the missing data and re-execute that task to regenerate the data. It may happen that the re-executed task also reports an input error. This would cause Tez to go up one more step in the DAG dependency and so on, until it has found stable intermediate data. The edge API allows for the specification of intermediate data resiliency such that Tez can be informed that a given edge data has been reliably stored, thus creating a barrier to cascading failures. Tez built-in input/output libraries leverage heuristics inherited from MapReduce for mitigating and recovering from network errors and cascading failures when shuffling large volumes of data. E.g. temporary network errors are retried with back-off before reporting an error event. Partially fetched data is cached and the consumer task stays alive until the remaining missing data is regenerated. The Tez AM periodically checkpoints its state. If the node, that is running the Tez AM, has a failure then YARN will restart the AM on another node and the AM can recover its state from the checkpoint data.

Tez tightly integrates with YARN to handle planned and unplanned cluster outages. It listens to notifications about machine loss or decommissioning and pro-actively re-executes the tasks that were completed on such machines. This decreases the chance that consumers of those task outputs will fail. Tez also understands actions taken by YARN such as preempting containers for capacity rebalancing or terminating badly behaving containers and responds to those actions appropriately.

Limitations. The current implementation of Tez is Java based and thus we are limited to JVM based applications right now. The Tez based MapReduce implementation has successfully executed non-JVM user code using MapReduce's approach of forking off non-Java code. However, a more native Tez support would need non-Java APIs for writing IPOs and executors to support them. Work is in progress to have a portable text based representation of the DAG API to enable non-Java compilers that target Tez. Tez can only be used in a YARN based Hadoop cluster because the current scheduling implementation has been written for YARN. Our recent work to enable developer debugging capability has abstracted out the dependence on a cluster. Extensions of that work could enable Tez to utilize other systems for execution. The current fault tolerance model depends on the assumption that intermediate task outputs are localized to the machine on which the task ran. Thus intermediate data loss causes re-execution of the task on a different machine. This may not be true of all IOs. e.g. if data is being streamed directly over the network. Also, such network streaming may result in collapse of the connected streaming sub-graph, which would need extensions of the fault tolerance model to handle such correlated failures.


5. APPLICATIONS & ADOPTION

In this section we will outline projects that have been updated or prototyped to use the Tez framework to run on YARN. These projects represent a significant variety of application types and help show the applicability of the Tez APIs for modeling and building high-performance data processing applications.


5.1 Apache MapReduce

MapReduce is a simple yet powerful means of scalable data processing that can be credited with ushering in the era of inexpensive hyper-scale data processing. At its core, it is a simple 2 vertex connected graph. In Tez, it can be represented with a map vertex and a reduce vertex that are connected using a scatter-gather edge. Tez has a built-in MapProcessor and a ReduceProcessor that run in the respective Map and Reduce vertices and provide the MapReduce interface functionality. Thus MapReduce can be easily written as a Tez based application and, in fact, the Tez project comes with a built-in implementation of MapReduce. Any MapReduce based application can be executed without change using the Tez version of MapReduce by simply changing a MapReduce configuration on a YARN cluster.


5.2 Apache Hive

Apache Hive is one of the most popular SQL-based declarative query engines in the Hadoop ecosystem. It used to translate queries written in HiveQL (a SQL-like dialect) to MapReduce jobs and run them on a Hadoop cluster. Like other SQL engines, Hive translates the queries into optimized query trees. Often these translations to MapReduce were inefficient due to the restricted expressiveness of MapReduce. These trees translate directly to DAGs specified using the Tez DAG API. Thus they can be represented efficiently in Tez. In addition, Hive uses custom edges (written to the Tez API) to perform sophisticated joins that were hitherto very difficult to do. E.g. In a join variant called Dynamically Partitioned Hash Join; Hive uses a custom vertex manager to determine which subsets of data shards to join with each other and creates a custom edge that routes the appropriate shards to their consumer tasks. While these query planning improvements provide algorithmic performance gains, Hive benefits from the execution efficiencies to get significant performance benefits out of the box. This integration has been implemented by the Apache Hive community with Hive 0.13 being the first release of Hive to have Tez integration. Further Tez-based optimizations (like dynamic partition pruning) have been released in Hive 0.14 with more work planned in future releases.


5.3 Apache Pig

Apache Pig provides a procedural scripting language (named PigLatin) that is designed to write complex batch processing ETL pipelines. The procedural nature of PigLatin allows the creation of complex DAGs with vertices having multiple outputs. In MapReduce, applications could write only 1 output and thus were forced to use creative workarounds like tagging the data or writing side-effect outputs. Being able to model multiple outputs explicitly via the Tez APIs allows the planning and execution code in Pig to be clean and maintainable. Pig supports joins with data-skew detection and this was earlier done by running different MapReduce jobs to read and sample the data, then create histograms based on the samples on the client machine and finally run another job that uses the histogram to read and partition the data. This complex workflow of jobs can now be represented as a sub-graph of any Pig DAG when using Tez. The samples are collected in a histogram vertex that calculates the histogram. The histogram is sent via an event to a custom vertex manager that re-configures the partition vertex to perform the optimal partitioning. Pig developers from Yahoo, Netflix, LinkedIn, Twitter and Hortonworks came together to implement this integration. Pig 0.14 is the first release of Pig to support Tez based execution in addition to MapReduce.


5.4 Apache Spark

Apache Spark is a new compute engine that provides an elegant language integrated Scala API for distributed data processing. It specializes in machine learning but the API lends itself to mixed workloads and pipeline processing. Data distribution metadata is captured at the language layer in a concept called Resilient Distributed Dataset (RDD) and this metadata is used during compilation to construct a DAG of tasks that perform the distributed computation. Spark comes with its own processing engine service to execute these tasks. We were able to encode the post-compilation Spark DAG into a Tez DAG and run it successfully in a YARN cluster that was not running the Spark engine service. User defined Spark code is serialized into a Tez processor payload and injected into a generic Spark processor that deserializes and executes the user code. This allows unmodified Spark programs to run on YARN using Spark's own runtime operators. Apache Hive and Pig were already designed to translate to MapReduce and the translation to Tez is an evolutionary step. Modeling a net new engine like Spark on YARN using Tez presents a strong proof of the generality and modeling power of the Tez framework. Tez sessions also enable Spark machine learning iterations to run efficiently by submitting the per-iteration DAGs to a shared Tez session. This work is an experimental prototype and not part of the Spark project.


5.5 Apache Flink

Apache Flink is a new project in the Apache community with roots in the Stratosphere research project of the TU Berlin data management community. It is a parallel processing engine that provides programming APIs in Java and Scala, a cost-based optimizer for these APIs, as well as its own execution engine. Flink is another example of a new platform that could be integrated with YARN using the Tez framework instead of running it as a standalone service. The post-optimization DAG is translated to a Tez DAG for this integration. While Apache Hive and Pig work on key-value data formats and could use the built-in Tez inputs and outputs, Flink keeps intermediate data in a custom binary format. This format can be used to perform operations like group by etc. without much deserialization overhead. The pluggable and composable Tez task model allowed Flink to incorporate its runtime operators and binary format inside Tez tasks, thus allowing unmodified programs to run on YARN using Flink's native runtime model. This work is currently in progress in the Apache Flink developer community.


5.6 Commercial Adoption

The customizability and performance focused design of Tez has resulted in rapid uptake from commercial software projects. Concurrent Inc. supports an open source language integrated API in Java, called Cascading, for distributed data processing. Cascading has been updated to run on YARN using the Tez framework with promising performance results. Scalding is a Scala dialect over Cascading that automatically gets the benefits of Tez via Cascading. Cascading 3.0 is currently available as a developer preview and integrates with Tez. Datameer provides a visual analytics platform that uses Tez to run optimized analytics queries on YARN. It also uses Tez sessions to maintain a query service pool for fast response times in a secure, multi-tenant environment. Datameer 5.0 is the first release that uses Tez. Release 8 of Syncsort's Hadoop product, DMX-h, shipped with an intelligent execution layer to enable transparent targeting of execution frameworks other than Mapreduce. Following this, they are in the process of integrating with Tez as one of their supported execution frameworks.


5.7 Deployments

Apache Tez has been deployed across multiple organizations and on a variety of cluster configurations. Most prominently, Yahoo! has deployed Tez on multiple clusters ranging from 100s to 1000s of nodes to run Hive and Pig with Tez. LinkedIn has completed migration of all their Hadoop clusters to YARN and is running Hive and Pig with Tez. Microsoft Azure has deployed Hive with Tez as part of its cloud Hadoop offering. Hortonworks has provided Tez as a part of its Hadoop distribution since April 2014 and is seeing rapid adoption of Tez by its install base. At the time of publication, nearly 100 Hortonworks customers have explored the capabilities of Tez.

The growing adoption of Tez we described in this section provides a qualitative metric of the project success. In the next section, we turn to the experiment results obtained as a result of these applications being integrated with Tez.


6. EXPERIMENTAL RESULTS

We devote this section to present several experiments, showcasing how Tez-based implementations of Hive, Pig and Spark on YARN outperform their original implementation on YARN. The experiments are derived from both standard benchmarks, and production workloads.


6.1 Hive 0.14 Performance Tests

Hive utilizes various features available in Tez, such as broadcast edges, runtime re-configuration and custom vertex managers, to achieve a better overall execution of the user's processing goals. In conjunction with Hive 0.14's Cost Based Optimizer, Tez enables the execution of bushy join plans which can take advantage of intermediate broadcast joins. The pluggable task model of Tez allows Hive to use custom vectorized operators throughout the processing. Custom edges are used to perform efficient Hive sort-merge-bucket joins. The Tez-based implementation substantially outperforms the traditional MapReduce based one on a TPC-DS derived Hive workload, run at 30 terabytes scale on a 20 node cluster with 16 cores, 256Gb RAM and 6 x 4Tb drives per node.


6.2 Yahoo Hive Scale Tests

A comparative scale test of Hive on Tez was done with a TPC-H derived Hive workload, at 10 terabytes scale on a 350 node research cluster with 16 cores, 24Gb RAM and 6 x 2Tb drives per node. This was presented at Hadoop Summit 2014, San Jose. This shows that Tez based implementation outperforms the MapReduce based implementation at large cluster scale.


6.3 Yahoo Pig Production Tests

At Yahoo!, Pig on Tez was tested on large production ETL pig jobs that run in the order of minutes to hours. To test different aspects of scale and features of the implementation, the pig scripts run had varying characteristics like terabytes of input, 100K+ tasks, complex DAGs with 20 to 50 vertices and doing a combination of various operations like group by, union, distinct, join, order by, etc. The tests were run on different production clusters where data resided and already running regular jobs with average utilization of 60-70%. The cluster had 4,200 servers, 46 PB HDFS storage and 90TB aggregate memory. Most data nodes were with 12/24G RAM, 2 x Xeon 2.40GHz, 6 x 2TB SATA on Hadoop 2.5, RHEL 6.5, JDK 1.7. There were performance improvements of 1.5 to 2x compared to MapReduce keeping all the configuration (memory, shuffle configuration, etc.) same as MapReduce.


6.4 Pig KMeans Iteration Tests

As noted in Section 4.2, Tez session and container-reuse features work in favor of fast iterative workloads, which require consecutive DAGs to execute over the same data-set. Performance improvements were demonstrated for a K-means iterative PIG script, run for 10, 50 and 100 iterations against a 10,000 row input dataset on a single node. This was presented at the Hadoop Summit 2014, San Jose.


6.5 Spark Multi-Tenancy on YARN Tests

As explained in Section 4.3, Tez's ephemeral task based model is better for multi-tenancy and resource-sharing. This is demonstrated by comparing service-based vs Tez-based implementations of Spark on YARN. The Tez based implementation releases idle resources that get assigned to other jobs that need them, thus speeding them up, while the service-based implementation holds on to resources for the life of the service. For the experiment, we have a 5-user concurrency test of partitioning a TPC-H lineitem data-set along the L_SHIPDATE column, on a 20 node cluster. The tests were run across data sets which correspond to 100 GB, 200 GB, 500 GB and 1 TB warehouse scale factors. The cluster used to run this workload was identical to the Hive 0.14 benchmarks, having 16 cores, 256Gb of RAM and 6 x 4Tb disks per node.


7. OPEN SOURCE AND FUTURE WORK

Apache Tez has been developed as an open source project under the Apache Software Foundation. It's a community driven project with contributors from Microsoft, Yahoo, Hortonworks, LinkedIn among others as well as individual enthusiasts. The source code for the project is available at http://git.apache.org/tez.git and the project website is at http://tez.apache.org.

The open architecture of Tez and its fundamentally customizable design lends it to becoming a platform for experimentation and innovation. We believe that the current use cases built on Tez are only the initial steps of a longer journey. There is considerable interest in a variety of areas to improve and leverage Tez. Progressive query optimization which allows a complex and large query to be executed partially and optimized incrementally as the query proceeds. Apache Hive and Apache Calcite are working together on materialized views for speeding up common sub-queries. We want to provide deep integration with in-memory storage capabilities being added to HDFS so that Tez applications can benefit from in-memory computing. Tez currently supports Java applications and extending it to support other languages would widen the scope of applications built using Tez. Another area of interest is tooling for debugging failure and performance bottlenecks. Increasingly, geographical distribution and legal/privacy requirements are making cross data-center job execution important. Improving the Tez orchestration and API to model such jobs may help in executing them efficiently. The above are only a few of the many possibilities in which Tez may be evolved or used by academic and commercial communities. Many runtime optimizations are also in the works. E.g. automatically choosing optimal data transport mechanisms like in-memory data for machine co-located tasks or using a reliable store for outputs of extremely long tasks so that their outputs are safeguarded against loss. A tactical idea is to create tooling that enables a full MapReduce workflow to be stitched into a single Tez DAG. This would enable legacy MapReduce workflows to easily use the MapReduce implementation in Tez.


8. RELATED WORK

Apache Tez has been fortunate to learn from the development and experiences of similar systems such as Dryad, Hyracks and Nephele. All of them share the concept of modeling data processing as DAGs with vertices representing application logic and edges or channels representing data transfer. Tez makes this more fine-grained by adding the concepts of inputs, processor and outputs to formally define the tasks executing the DAGs, leading to clear separation of concerns and allowing pluggable task composition. All of them participate to varied extents in the data plane and define some form of data format, which allows applications to define custom formats that derive from the base definition. All of them define on-disk, over-network and in-memory communication channels. Tez, on the other hand, does not define any data format and is not part of the data plane at all. On a similar note, Hyracks defines an operator model for execution that allows it to understand the data flow better for scheduling. Tez treats processors as black boxes so that the application logic can be completely decoupled from the framework. Nephele is optimized for cloud environments where it can elastically increase or decrease resources and choose appropriate virtual machines. Tez also enables resource elasticity by acquiring and releasing resources in YARN. Dryad and Tez share the concept of vertex managers for dynamic graph re-configurations. Tez takes this concept a step further by formalizing an API that allows the managers to be written without knowing the internals of the framework and also defining an event based communication mechanism that enables application code in tasks to communicate with application code in vertex managers in order to actuate the re-configurations. In addition, Tez adds the concept of input initializers to formally model primary data sources and apply runtime optimizations while reading them. Dryad schedules tasks when all the inputs of the tasks are ready to be consumed, to prevent scheduling deadlocks. Tez allows out of order execution for performance reasons and has built-in preemption to resolve scheduling deadlocks. Overall, Tez differs from these systems in its modeling capabilities and the design goal of being a library to build engines rather than being an engine by itself. MapReduce is, of course, the incumbent engine in the Hadoop ecosystem. Tez subsumes the MapReduce APIs such that it is possible to write a fully functional MapReduce application using Tez.

Dremel is a processing framework for interactive analysis of large data sets based on multi-level execution trees that is optimized for aggregation queries and has motivated systems like Presto and Apache Drill. These, and other SQL query engines like Impala or Apache Tajo, differ from Tez by being engines optimized for specific processing domains whereas Tez is a library to build data processing applications. Spark is a new general purpose data processing engine. It exposes a Resilient Distributed Dataset (RDD) based computation model that eventually gets executed on an in-memory storage and compute engine. Tez, again differs being a library and not a general purpose engine. Tez does not provide any storage service but applications can use existing in-memory stores, e.g. HDFS memory storage, to get the advantage of in-memory computing. The Spark notion of using RDDs as a means of implicitly capturing lineage dependency between steps of processing can be related to capturing that same dependency explicitly via defining the DAG using Tez APIs.

An important category of systems to compare against are other frameworks to build YARN-applications. The two most relevant in this space are Apache REEF and Apache Twill. These systems focus on a much broader class of applications (including services), than Tez, and thus provide a lower-level API. Tez focuses on supporting data-flow driven applications, and thus consciously chooses to provide a structured DAG-based control-flow.


9. CONCLUSIONS

Today, Hadoop is a booming ecosystem for large-scale data processing, blessed with an ever growing set of application frameworks, providing diverse abstractions to process data. We recognize that this is invaluable, yet we highlight substantial concerns of fragmentation and repeated work, as each application framework solves similar fundamental problems over and over again.

To address this issue we present Apache Tez, an open-source framework designed to build data-flow driven processing engines. Tez provides a scaffolding and libraries to facilitate the design and implementation of DAG-centric data processing applications, and focuses on re-use, while balancing customizability of the performance critical data plane. Tez makes a conscious effort to enable dynamic optimizations, such as partition pruning. Besides these key architectural choices, what sets Tez apart from other attempts of unifying frameworks is a sizeable open-source community, that is pushing Tez towards becoming the framework of choice for building DAG-oriented data processing engines. As of today, the most popular projects (Hive, Pig and Cascading) have integrated with Tez. We demonstrated experimentally that the Tez-based incarnations of these systems deliver substantial performance benefits beyond the qualitative argument of leveraging common functionalities.

We argue that the standardization we are promoting can foster even faster innovation, and enable integration plays that would be otherwise cumbersome (e.g., pipelines made up of multiple application frameworks). Tez's customizability and open-source community makes it an ideal playground for research, as novel ideas can be tested, integrated, and gain real-world impact with minimal overhead.


Acknowledgements

Apache Tez is an open source community driven project with contributions gratefully accepted from numerous individuals and organizations. In particular we would like to call out Rajesh Balamohan for keeping a watchful eye on performance and Tassapol Athiapinya and Yesha Vora for testing and system validation. We would like to thank members of other project communities who have helped in adopting and demonstrating the value of Tez. Notably, Gunther Hagleitner and Vikram Dixit for Apache Hive; Rohini Palaniswamy, Cheolsoo Park, Daniel Dai, Olga Natkovich, Mark Wagner and Alex Bain for Apache Pig; Chris Wensel for Cascading; Oleg Zhurakousky for Apache Spark; Kostas Tzoumas and Stephan Ewen for Apache Flink. We are also grateful to Yahoo and Hortonworks for providing experimentation infrastructure. We hope that the innovation platform provided by Tez will lead to further contributions from many more.

Source: apache_tez · apache_tez.md · updated 2026-05-29 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

Apache Tez: A Deep Dive Into Architecture, Internals & Source Code

Table of Contents

  1. What Is Apache Tez?
  2. Why Tez Exists — The MapReduce Problem
  3. Architecture Overview
  4. Core Concepts
  5. Source Code Structure
  6. Deep Dive: Key Modules
  7. The DAG Execution Lifecycle
  8. The Shuffle Pipeline
  9. Runtime Reconfiguration
  10. How Hive and Pig Use Tez
  11. Tez on YARN — The Full Picture
  12. Reading the Source: A Guided Path
  13. Simpler Open Source Alternatives
  14. References

1. What Is Apache Tez?

Apache Tez is a distributed data-processing execution engine built on top of Apache Hadoop YARN. It generalizes the rigid two-phase MapReduce model into an arbitrary directed acyclic graph (DAG) of tasks, enabling higher-level frameworks like Hive and Pig to express complex query plans as a single, optimized execution graph rather than a chain of independent MapReduce jobs.

Tez is not an end-user framework. You don't typically write Tez applications directly (though you can). Instead, Tez acts as the execution backend for tools like:

  • Apache Hive (SQL on Hadoop)
  • Apache Pig (data flow scripting)
  • Cascading (Java data pipelines)

Key Stats

MetricValue
LanguageJava
Lines of Code~225,000
Contributors109+
First CommitApril 2013
LicenseApache 2.0
Repositorygithub.com/apache/tez

2. Why Tez Exists — The MapReduce Problem

The Pain Points of MapReduce

Classic MapReduce forces every computation into a rigid two-phase pattern:

Input → Map → Shuffle/Sort → Reduce → Output (HDFS)

For a complex Hive query that involves multiple joins and aggregations, MapReduce had to chain several independent jobs:

MR Job 1: Scan + Filter     → write to HDFS
MR Job 2: Join               → read from HDFS, write to HDFS
MR Job 3: Aggregate          → read from HDFS, write to HDFS
MR Job 4: Final sort + output → read from HDFS, write to HDFS

Each boundary between jobs meant:

  • Materializing intermediate data to HDFS (disk I/O + replication overhead)
  • Launching new YARN containers (JVM startup latency)
  • No pipelining between stages (the next job can't start until the previous one fully finishes writing)

What Tez Changes

Tez lets you express the same computation as a single DAG:

Scan+Filter ─┐
              ├─→ Join ─→ Aggregate ─→ Sort ─→ Output
Scan+Filter ─┘

Intermediate data flows through in-memory edges (or local disk, never HDFS) between vertices in the same DAG. Containers are reused across vertices. The entire plan runs as one YARN application, not four.

Performance Impact

For Hive queries, switching from MapReduce to Tez typically yields:

  • 2–5x faster for simple queries (fewer job launches, no HDFS writes between stages)
  • 10–100x faster for complex multi-stage queries (eliminates cascading HDFS I/O)
  • Near-interactive latency for many queries that previously took minutes

3. Architecture Overview

Tez has a clean three-layer architecture:

┌─────────────────────────────────────────────────┐
│              CLIENT APPLICATION                  │
│  (Hive, Pig, or custom Tez app)                  │
│                                                   │
│  Uses DAG API to build: Vertices + Edges          │
│  Submits DAG to the Tez ApplicationMaster         │
└────────────────────┬────────────────────────────┘
                     │ Submit DAG
                     ▼
┌─────────────────────────────────────────────────┐
│          TEZ APPLICATION MASTER (AM)             │
│  (runs as a YARN ApplicationMaster)              │
│                                                   │
│  • DAG state machine (DAGImpl)                    │
│  • Vertex state machine (VertexImpl)              │
│  • Task state machine (TaskImpl)                  │
│  • TaskAttempt state machine (TaskAttemptImpl)    │
│  • Container management & reuse                   │
│  • Speculative execution                          │
│  • Runtime reconfiguration                        │
│  • Scheduling & data locality                     │
└────────────────────┬────────────────────────────┘
                     │ Launch tasks in containers
                     ▼
┌─────────────────────────────────────────────────┐
│            TASK RUNTIME (per container)           │
│                                                   │
│  • Input (reads data: HDFS, shuffle, etc.)        │
│  • Processor (user logic: map, reduce, join)      │
│  • Output (writes data: shuffle, HDFS, etc.)      │
│                                                   │
│  Tasks run the Input-Processor-Output pipeline    │
│  within YARN containers on cluster nodes          │
└─────────────────────────────────────────────────┘

The Two Core Components (Per the Tez README)

At its heart, Tez has just two components:

  1. The data-processing pipeline engine — where you plug in Input, Processor, and Output implementations to perform arbitrary data processing within each task.

  2. The DAG-based application master — where you compose those tasks into a DAG and the master handles scheduling, fault tolerance, and resource negotiation with YARN.


4. Core Concepts

DAG (Directed Acyclic Graph)

The top-level execution unit. A DAG contains vertices connected by edges. It maps to a single YARN application.

DAG dag = DAG.create("my-query");
dag.addVertex(scanVertex);
dag.addVertex(joinVertex);
dag.addEdge(Edge.create(scanVertex, joinVertex, edgeConfig));

Vertex

A logical stage in the pipeline. Each vertex has:

  • A parallelism (number of tasks)
  • A Processor class (the computation logic)
  • Zero or more Inputs and Outputs

A vertex with parallelism=100 runs 100 tasks, each executing the same Processor on different data partitions.

Edge

Defines how data flows between two vertices. An edge specifies:

  • Data movement type: ONE_TO_ONE, SCATTER_GATHER, BROADCAST
  • Scheduling type: SEQUENTIAL (downstream waits) or CONCURRENT
  • Data source type: PERSISTED (written to disk) or EPHEMERAL (pipelined)
  • Edge manager plugin: controls routing of data between producer and consumer tasks

Task

A single unit of work within a vertex. Runs in a YARN container with an Input → Processor → Output pipeline.

Input / Processor / Output

The pluggable components of each task:

  • Input: reads key-value pairs (from HDFS, from shuffle, from another source)
  • Processor: transforms data (the user's computation logic)
  • Output: writes key-value pairs (to shuffle, to HDFS, to another sink)

This is the fundamental abstraction. MapReduce hardcodes the inputs and outputs; Tez makes them pluggable.


5. Source Code Structure

The repository (github.com/apache/tez) is organized into these Maven modules:

tez/
├── tez-api/                  # Public-facing API (DAG, Vertex, Edge, configs)
├── tez-common/               # Shared utilities, counters, security
├── tez-dag/                  # ★ THE CORE: ApplicationMaster, state machines
├── tez-runtime-internals/    # ★ Task runtime framework (Input/Processor/Output lifecycle)
├── tez-runtime-library/      # ★ Built-in I/O implementations (shuffle, sort, merge)
├── tez-mapreduce/            # MapReduce compatibility layer
├── tez-examples/             # WordCount, OrderedWordCount examples
├── tez-plugins/              # History logging, YARN plugin
├── tez-tests/                # Integration tests
├── tez-ui/                   # Web UI (Ember.js)
└── tez-dist/                 # Distribution packaging

Complexity by Module

ModuleApprox. LOCComplexityWhat It Does
tez-api~20kMediumUser-facing API: DAG, Vertex, Edge builders, configuration
tez-dag~70kVery HighApplicationMaster: DAG/Vertex/Task state machines, scheduling, container mgmt
tez-runtime-internals~15kHighTask-level runtime: manages the I/P/O pipeline lifecycle inside containers
tez-runtime-library~30kVery HighShuffle, sort, merge, partitioning — the data movement engine
tez-mapreduce~15kMediumWraps MR Mapper/Reducer to run as Tez Processors
tez-common~5kLowUtilities, counters
tez-examples~2kLowBest starting point for reading the code
tez-plugins~10kMediumHistory event logging, YARN task communicator
tez-ui~20kMediumWeb interface (JavaScript/Ember.js)

6. Deep Dive: Key Modules

6.1 tez-api — The Public API

This is the module that Hive, Pig, and custom applications interact with. The key classes:

DAG.java

The builder for a DAG. Lets you:

  • Create a named DAG
  • Add vertices and edges
  • Set DAG-level configuration and credentials
  • Attach security tokens
DAG dag = DAG.create("word-count");
Vertex tokenizer = Vertex.create("Tokenizer", ProcessorDescriptor.create(
    TokenProcessor.class.getName()), numTasks);
Vertex summation = Vertex.create("Summation", ProcessorDescriptor.create(
    SumProcessor.class.getName()), numTasks);

dag.addVertex(tokenizer)
   .addVertex(summation)
   .addEdge(Edge.create(tokenizer, summation, edgeConfig));

Vertex.java

Defines a processing stage. Configuration includes:

  • Processor class
  • Parallelism (task count)
  • Data sources (inputs that don't come from other vertices)
  • Environment settings (JVM opts, environment variables)
  • Vertex manager plugin (for dynamic parallelism)

Edge.java

Connects two vertices. The EdgeProperty specifies:

EdgeProperty.create(
    DataMovementType.SCATTER_GATHER,  // How data is routed
    DataSourceType.PERSISTED,          // Disk or in-memory
    SchedulingType.SEQUENTIAL,         // When downstream starts
    outputDescriptor,                  // Output class on producer side
    inputDescriptor                    // Input class on consumer side
);

Data movement types — the critical routing strategies:

TypeDescriptionUse Case
ONE_TO_ONETask i in source → Task i in destPipeline stages with same parallelism
SCATTER_GATHERAll source tasks → all dest tasks (with partitioning)Shuffle/sort (like MR's shuffle)
BROADCASTEvery source task sends to every dest taskBroadcast joins (small table to all reducers)
CUSTOMUser-defined routing via EdgeManagerPluginSpecialized data routing

TezClient.java

The client-side entry point. Creates a YARN application, uploads the DAG plan, and submits it to the Tez ApplicationMaster.


6.2 tez-dag — The ApplicationMaster (The Hard Core)

This is where most of the complexity lives. The AM runs as a YARN ApplicationMaster and manages the entire DAG execution.

State Machines

The AM is built around four nested state machines, each implemented with Hadoop's StateMachineFactory:

DAGImpl (DAG lifecycle)
 └── VertexImpl (per-vertex lifecycle)
      └── TaskImpl (per-task lifecycle)
           └── TaskAttemptImpl (per-attempt lifecycle)
DAGImpl.java — DAG State Machine

States: NEW → INITED → RUNNING → COMMITTING → SUCCEEDED / FAILED / KILLED / ERROR

Key transitions:

EventFrom StateTo StateWhat Happens
DAG_INITNEWINITEDParse DAG plan, create VertexImpl objects
DAG_STARTINITEDRUNNINGStart root vertices (those with no inputs from other vertices)
VERTEX_COMPLETEDRUNNINGRUNNINGCheck if downstream vertices can start
ALL_VERTICES_DONERUNNINGCOMMITTINGAll vertices succeeded, commit outputs
DAG_COMPLETEDCOMMITTINGSUCCEEDEDFinal commit done
INTERNAL_ERRORanyERRORUnrecoverable failure

This state machine orchestrates the entire execution. When a vertex completes, the DAG checks which downstream vertices have all their input-vertices completed and starts them.

Tip: You can generate a visual Graphviz diagram of this state machine:

mvn compile -Pvisualize \
  -Dtez.dag.state.classes=org.apache.tez.dag.app.dag.impl.DAGImpl \
  -DskipTests=true

This outputs Tez.gv which you can render with dot -Tpng Tez.gv -o dag-states.png.

VertexImpl.java — Vertex State Machine

States: NEW → INITIALIZING → INITED → RUNNING → COMMITTING → SUCCEEDED / FAILED / KILLED

This is the most complex state machine. A vertex:

  1. Initializes its tasks based on input splits or configured parallelism
  2. Waits for the VertexManager to signal readiness
  3. Schedules tasks on available containers
  4. Tracks task completion, handles failures and retries
  5. Commits output when all tasks succeed

Key complexity here: vertex reconfiguration. At runtime, a vertex can change its parallelism based on actual data sizes (see Section 9).

TaskImpl.java and TaskAttemptImpl.java

Task manages one logical unit of work. TaskAttempt tracks a single execution attempt of that task (there may be multiple attempts due to failures or speculative execution).

TaskAttempt handles:

  • Container assignment
  • Launch on a NodeManager
  • Status updates and heartbeats
  • Output commit
  • Failure handling (retry logic)

Container Management

The AM doesn't just schedule tasks — it manages container reuse. Key classes:

  • TaskSchedulerManager — coordinates container requests with YARN
  • ContainerLauncherManager — launches tasks in containers
  • TaskCommunicatorManager — heartbeat protocol with running tasks

Container reuse is a critical optimization. When a task finishes, the AM can reassign the same container to the next task in the DAG without releasing it back to YARN and requesting a new one. This avoids the JVM startup penalty that made chained MapReduce jobs slow.


6.3 tez-runtime-internals — The Task Runtime

This module manages the lifecycle of a single task running inside a YARN container.

LogicalIOProcessorRuntimeTask.java

The main class that runs inside each container. It:

  1. Initializes the configured Inputs
  2. Initializes the Processor
  3. Initializes the configured Outputs
  4. Calls processor.run(inputs, outputs) — this is where user code executes
  5. Handles cleanup and status reporting

The Input-Processor-Output Model

┌──────────┐     ┌──────────────┐     ┌───────────┐
│  Input 1  │────→│              │────→│  Output 1  │
├──────────┤     │   Processor   │     ├───────────┤
│  Input 2  │────→│              │────→│  Output 2  │
└──────────┘     └──────────────┘     └───────────┘

Each component is pluggable:

  • Inputs implement LogicalInput — they know how to read from a data source
  • Processors implement LogicalIOProcessor — they transform data
  • Outputs implement LogicalOutput — they know how to write to a destination

The task runtime wires them together based on the DAG plan. This is the key abstraction that makes Tez flexible — MapReduce hardcodes the wiring; Tez lets you plug in any combination.


6.4 tez-runtime-library — Built-in I/O Implementations

This is the other highly complex module. It provides the actual data movement implementations.

Key Classes

ClassPurpose
OrderedPartitionedKVOutputThe "map output" — partitions, sorts, and optionally combines data for a scatter-gather edge
OrderedGroupedKVInputThe "reduce input" — fetches shuffled data, merges sorted streams
UnorderedKVOutputUnpartitioned output (for broadcast edges)
UnorderedKVInputReads broadcast data
ShuffleManagerCoordinates fetching shuffle data from upstream tasks
MergeManagerManages the merge of sorted runs (in-memory and on-disk)
IFile / IFileOutputStreamThe intermediate file format for shuffle data
ExternalSorterSorts data in memory, spills to disk when memory is exhausted

The Shuffle Pipeline (Scatter-Gather)

This is the most performance-critical code path. When a producer vertex sends data to a consumer vertex via a SCATTER_GATHER edge:

Producer side (OrderedPartitionedKVOutput):

Records from Processor
  → Partition (by key, using configured Partitioner)
  → Sort (in-memory sort buffer)
  → Optional combine (reduce locally before shipping)
  → Spill to disk when buffer full
  → Merge spills into a single sorted, partitioned file

Consumer side (OrderedGroupedKVInput):

Fetch partition data from all producer tasks (HTTP)
  → Merge sorted streams (k-way merge)
  → Present to Processor as a sorted key-value stream

This is conceptually the same as MapReduce's shuffle, but with two critical improvements:

  1. No HDFS materialization — intermediate data goes to local disk or stays in memory
  2. Pipelining — consumers can start fetching before all producers finish (configurable via slow-start)

7. The DAG Execution Lifecycle

Here's what happens when Hive submits a query via Tez:

Step 1: DAG Construction (Client Side)

Hive's query optimizer produces a physical plan. The Tez execution engine (inside Hive) translates this into a Tez DAG:

Hive Query: SELECT dept, COUNT(*) FROM employees GROUP BY dept

Tez DAG:
  Vertex "Map 1" (parallelism=10) → reads employees table from HDFS
      │
      │ SCATTER_GATHER edge (partition by dept)
      ▼
  Vertex "Reducer 2" (parallelism=4) → aggregates counts, writes to HDFS

Step 2: DAG Submission

The TezClient submits the DAG:

  1. Serializes the DAG plan as a Protocol Buffer message
  2. Uploads the plan + JARs + configuration to HDFS
  3. Submits a YARN application (or reuses an existing Tez session)

Step 3: ApplicationMaster Initialization

The Tez AM starts on a cluster node:

  1. DAGAppMaster.main() — entry point
  2. Registers with YARN ResourceManager
  3. Deserializes the DAG plan
  4. Creates DAGImpl, which creates VertexImpl objects for each vertex

Step 4: Vertex Scheduling

The DAG starts root vertices (those with no upstream dependencies):

  1. VertexImpl determines parallelism (from input splits or configured value)
  2. VertexImpl creates TaskImpl objects
  3. Each task requests a YARN container (with data locality preferences)
  4. The TaskSchedulerManager negotiates containers from the ResourceManager

Step 5: Task Execution

For each scheduled task:

  1. The AM launches the task in an allocated container
  2. TezTaskRunner starts in the container's JVM
  3. The runtime initializes Input → Processor → Output
  4. Processor.run() executes (this is where the actual computation happens)
  5. The task reports status back to the AM via heartbeats

Step 6: Inter-Vertex Data Flow

When "Map 1" tasks produce output:

  1. OrderedPartitionedKVOutput writes sorted, partitioned data to local disk
  2. The AM notifies "Reducer 2" that data is available
  3. "Reducer 2" tasks fetch their partitions from "Map 1" tasks via HTTP
  4. ShuffleManager + MergeManager handle the fetch and merge

Step 7: Completion

  1. All tasks in the final vertex complete
  2. Output is committed (to HDFS or wherever configured)
  3. The DAG transitions to SUCCEEDED
  4. The AM reports completion to the client
  5. Containers are released (or held for session reuse)

8. The Shuffle Pipeline

The shuffle is the most performance-sensitive code in Tez. Here's how data moves through it in detail.

Producer Side

The flow in OrderedPartitionedKVOutput:

1. Processor writes key-value pairs
2. Each record is:
   a. Partitioned → which downstream task gets it
   b. Serialized → converted to bytes
   c. Written to a circular in-memory buffer (sort buffer)
3. When buffer reaches threshold (default 80%):
   a. Sort the buffer by (partition, key)
   b. Optionally run combiner on each partition's data
   c. Spill sorted data to local disk as an IFile
4. After all records:
   a. Merge all spill files into a single output file
   b. Create an index file mapping partition → offset in the output file
   c. Register the output with the AM

Key configuration:

PropertyDefaultDescription
tez.runtime.io.sort.mb100 MBSize of the in-memory sort buffer
tez.runtime.sort.spill.percent0.8Buffer threshold that triggers a spill
tez.runtime.combine.min.spills3Minimum spills before combiner runs

Consumer Side

The flow in OrderedGroupedKVInput:

1. ShuffleManager determines which source tasks to fetch from
2. For each source task:
   a. HTTP GET to fetch this consumer's partition from the source's output
   b. If small enough → keep in memory
   c. If too large → write to local disk
3. MergeManager performs k-way merge:
   a. In-memory merge when memory segments exceed threshold
   b. On-disk merge when disk segments exceed threshold
   c. Final merge produces a single sorted stream
4. Processor reads merged sorted key-value pairs

The merge uses a priority queue (min-heap) over sorted segments — the same algorithm as MapReduce, but with better memory management and configurable thresholds.


9. Runtime Reconfiguration

One of Tez's most powerful features — and a major source of code complexity.

The Problem

When building the DAG, you often don't know the right parallelism for downstream vertices. Example: if "Map 1" produces 10 GB of shuffle data, you might want 100 reducers. If it produces 100 MB, you might want 2.

MapReduce forces you to guess at job submission time. Get it wrong, and you either waste resources (too many reducers) or create a bottleneck (too few).

How Tez Solves It

VertexManager plugins can dynamically reconfigure a vertex at runtime based on actual data from upstream:

public class ShuffleVertexManager extends VertexManagerPlugin {

    @Override
    public void onVertexManagerEventReceived(VertexManagerEvent event) {
        // Receive actual output size from upstream tasks
        actualOutputSize += event.getOutputSize();
    }

    @Override
    public void onVertexStateUpdated(VertexStateUpdate update) {
        // When enough upstream tasks have reported:
        int newParallelism = actualOutputSize / desiredTaskInputSize;
        getContext().reconfigureVertex(newParallelism, ...);
    }
}

ShuffleVertexManager (built-in) is the most important VertexManager. It:

  1. Collects output-size statistics from completed upstream tasks
  2. Estimates the total output size
  3. Calculates the optimal parallelism for the downstream vertex
  4. Reconfigures the vertex (changes task count and routing) before tasks launch

This means a Hive query doesn't need SET mapreduce.job.reduces=100 — Tez figures it out automatically.

Key Source Files

  • ShuffleVertexManager.java in tez-runtime-library — the auto-parallelism logic
  • VertexImpl.java — handles the reconfigure event, re-creates tasks
  • VertexManager.java in tez-api — the plugin interface

10. How Hive and Pig Use Tez

Hive on Tez

When you run a Hive query with hive.execution.engine=tez:

  1. Hive compiles the HiveQL into an operator tree (Scan → Filter → Join → Aggregate → File)
  2. Hive's optimizer (Calcite-based) optimizes the plan
  3. TezCompiler translates the operator tree into a Tez DAG:
    • Each "work" unit (a group of operators that run together) becomes a Vertex
    • Dependencies between work units become Edges
    • Shuffle boundaries (GROUP BY, JOIN, DISTRIBUTE BY) become SCATTER_GATHER edges
    • Broadcast joins become BROADCAST edges
  4. TezSessionState submits the DAG to a running Tez session (or starts a new one)
  5. Tez executes the DAG on YARN
  6. Hive reads the output from HDFS

Example: Multi-Stage Query

SELECT d.name, COUNT(*)
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE e.salary > 50000
GROUP BY d.name
ORDER BY COUNT(*) DESC;

As MapReduce (3 separate jobs):

Job 1: Scan employees, filter salary > 50000, scan departments → Join → HDFS
Job 2: Read join output → Group by dept name, count → HDFS
Job 3: Read aggregated output → Sort by count DESC → HDFS

As Tez (1 DAG, 4 vertices):

Vertex "Map 1" (scan employees + filter) ──┐
                                            ├─→ Vertex "Join" ─→ Vertex "GroupBy" ─→ Vertex "Sort"
Vertex "Map 2" (scan departments) ─────────┘

No HDFS writes between stages. Containers reused. Dynamic parallelism at each boundary.

Pig on Tez

Pig's integration is similar. Pig's TezCompiler converts Pig's logical plan into a Tez DAG. Each Pig operator (LOAD, FILTER, GROUP, FOREACH, STORE) maps to Tez vertices and edges.


11. Tez on YARN — The Full Picture

Session Mode vs. Non-Session Mode

Non-session mode: Each DAG submission creates a new YARN application. The AM starts, runs the DAG, and exits. Simple but has launch overhead.

Session mode: A long-lived Tez AM stays running between DAG submissions. Multiple DAGs can be submitted to the same session sequentially. This is what Hive uses for interactive queries — the first query pays the AM startup cost, but subsequent queries start immediately.

TezClient (session mode)
  → DAG 1: submitted, runs, completes
  → DAG 2: submitted immediately (no AM restart), runs, completes
  → DAG 3: ...
  → session.stop() — AM releases all containers and exits

Container Reuse

Within a session (and even within a single DAG), Tez reuses containers:

  1. Task A finishes in container C on node N
  2. The AM checks if any pending task prefers node N (data locality)
  3. If yes: reassign container C to that task — no YARN negotiation needed
  4. The container's JVM runs the new task's Input/Processor/Output

This is managed by AMContainerMap and the TaskSchedulerManager in tez-dag.

Speculative Execution

Tez supports speculative execution: if a task is running significantly slower than its peers, the AM launches a duplicate attempt on a different node. Whichever finishes first wins; the other is killed.

Controlled by: tez.am.speculation.enabled=true


12. Reading the Source: A Guided Path

If you want to understand how Tez works by reading the code, follow this sequence:

Level 1: The API (1–2 hours)

Start with the examples and the public API to understand the programming model.

  1. tez-examples/WordCount.java (~200 lines) — A complete Tez application. Shows how to define vertices, edges, processors, and submit a DAG. This is the "Hello World" of Tez.

  2. tez-examples/OrderedWordCount.java — Adds a second vertex for sorting. Shows a multi-vertex DAG with a SCATTER_GATHER edge.

  3. tez-api/DAG.java — Read the builder methods. Clean API.

  4. tez-api/Vertex.java — How vertices are configured.

  5. tez-api/Edge.java + EdgeProperty.java — How edges are defined. Pay attention to DataMovementType.

Level 2: The Task Runtime (2–4 hours)

Understand what happens inside each task.

  1. tez-runtime-internals/LogicalIOProcessorRuntimeTask.java — The task entry point. Follows the I/P/O lifecycle clearly.

  2. tez-api/Processor.java interface — Simple: just run(Map<String, LogicalInput>, Map<String, LogicalOutput>).

  3. tez-runtime-library/OrderedPartitionedKVOutput.java — The producer side of shuffle. Follow from write() through sort and spill.

  4. tez-runtime-library/OrderedGroupedKVInput.java — The consumer side. Follow from initialization through fetch and merge.

Level 3: The ApplicationMaster (4–8 hours)

The hardest part. Read the state machines.

  1. Generate the state diagram first:

    mvn compile -Pvisualize \
      -Dtez.dag.state.classes=org.apache.tez.dag.app.dag.impl.DAGImpl \
      -DskipTests=true
    

    Render the .gv file and keep it open as a reference.

  2. tez-dag/DAGImpl.java — Focus on the StateMachineFactory at the top of the file. Each .addTransition() call defines one edge in the state diagram. Read the transition handlers to understand what happens at each step.

  3. tez-dag/VertexImpl.java — The most complex file. Focus on:

    • The state machine definition
    • handleInitEvent() — how a vertex initializes
    • scheduleTasks() — how tasks are scheduled
    • reconfigureVertex() — runtime parallelism changes
  4. tez-dag/TaskImpl.java — Simpler. Focus on attempt management.

  5. tez-dag/TaskAttemptImpl.java — Focus on launch, completion, and failure handling.

Level 4: Container & Scheduling (Advanced)

  1. tez-dag/TaskSchedulerManager.java — How the AM interacts with YARN for containers.

  2. tez-dag/AMContainerMap.java — Container reuse logic.

  3. tez-runtime-library/ShuffleVertexManager.java — Auto-parallelism. The onVertexManagerEventReceived() and reconfigureVertex() methods are where the magic happens.


13. Simpler Open Source Alternatives

If the Tez codebase feels overwhelming, these projects implement the same core ideas in much less code. Study them first to build intuition, then come back to Tez.

For Understanding DAG Execution Logic

Luigi (Python, by Spotify)

Repository: github.com/spotify/luigi Size: ~15,000 lines of Python Best for: Understanding DAG dependency resolution and task scheduling

Luigi implements the core job of Tez's DAG engine — scheduling tasks in dependency order, handling retries, tracking state — in readable Python. The key files:

  • scheduler.py — the central scheduler that resolves dependencies
  • worker.py — the worker that pulls and executes tasks
  • task.py — the Task base class with requires() and run()

The requires() pattern maps directly to Tez's Edge concept — each task declares its upstream dependencies.

Dask (Python)

Repository: github.com/dask/dask Size: ~100k total, but local.py is ~400 lines Best for: Understanding the minimal DAG execution algorithm

Dask's dask/local.py contains a complete single-machine DAG executor in under 400 lines. It's the clearest possible implementation of the core algorithm:

  1. Build a DAG of function calls
  2. Identify tasks with no dependencies
  3. Execute them
  4. Remove completed tasks from the graph
  5. Repeat until empty

For the distributed version, distributed/scheduler.py adds work-stealing and data locality — concepts directly relevant to Tez's TaskSchedulerManager.

Prefect (Python)

Repository: github.com/PrefectHQ/prefect Size: ~50,000 lines Best for: Modern task/flow model with clean state management

Prefect's Task and Flow abstractions map almost 1:1 to Tez's Processor and DAG. Their state machine is simpler and better documented than Tez's.


For Understanding Distributed Execution

Ray (Python / C++)

Repository: github.com/ray-project/ray Best for: Understanding distributed task execution without YARN complexity

Ray's task model (remote functions, object store, scheduling) is conceptually the closest modern equivalent to Tez. Their architecture whitepaper and documentation are excellent. The key insight Ray shares with Tez: tasks produce objects, and downstream tasks consume those objects — the system handles transfer.

Spark Core (Scala)

Repository: github.com/apache/spark Best for: Comparing Tez's approach to the main competing engine

Spark's DAGScheduler plays the same role as Tez's DAGImpl + VertexImpl. Comparing the two is illuminating:

ConceptTezSpark
Execution unitDAGJob
StageVertexStage
TaskTaskTask
ShuffleOrderedPartitionedKVOutputShuffleMapTask
Dynamic parallelismShuffleVertexManagerAdaptive Query Execution
Container reuseAM-managedExecutor model (always reused)

For Understanding the Shuffle Pipeline

There's no great "simple" version of a distributed shuffle, because it's inherently complex. The most readable reference implementations:

  • Spark's SortShuffleWriter and ExternalSorter — same concepts as Tez's OrderedPartitionedKVOutput, but in Scala with better comments
  • Hadoop MapReduce's MapOutputBuffer — the original implementation that Tez's shuffle is based on (in hadoop-mapreduce-client-core)

Summary Comparison Table

ProjectLanguageLines of CodeWhat It TeachesLearning Time
Dask local.pyPython~400Minimal DAG executor algorithm1 hour
LuigiPython~15kDependency resolution, scheduling, retry1 day
Prefect corePython~50kTask/flow model, modern state management1–2 days
Tez examplesJava~2kTez API surface, I/P/O model2 hours
Ray corePython/C++LargeDistributed object-based task execution2–3 days
Spark DAGSchedulerScala~5kStage-based DAG execution (Tez competitor)1 day
Tez tez-dagJava~70kProduction DAG AM with full state machines1 week+
Tez tez-runtime-libraryJava~30kProduction shuffle/sort/merge pipeline1 week+
  1. Read Dask local.py — understand the core algorithm (1 hour)
  2. Read Luigi's scheduler.py — add dependency resolution and retry (half day)
  3. Read Tez tez-examples/WordCount.java — see how Tez exposes the DAG API (1 hour)
  4. Read Tez tez-api/DAG.java + Vertex.java + Edge.java — the user-facing API (2 hours)
  5. Generate Tez's state diagram — visualize the AM's state machine (30 min)
  6. Read Tez DAGImpl.java with the state diagram open — the AM core (half day)
  7. Read Tez OrderedPartitionedKVOutput.java — the shuffle producer (half day)

14. References

Official Resources

  • Apache Tez Website: https://tez.apache.org
  • GitHub Repository: https://github.com/apache/tez
  • Tez Design Documents: https://cwiki.apache.org/confluence/display/TEZ
  • How to Contribute: https://cwiki.apache.org/confluence/display/TEZ/How+to+Contribute+to+Tez

Key Papers & Talks

  • "Apache Tez: Accelerating Hadoop Query Processing" — Bikas Saha, Arun Murthy (Hortonworks, 2013)
  • "Hive + Tez: A Performance Deep Dive" — Jitendra Pandey, Gopal V
  • InfoQ Article: "What is Apache Tez?" — https://www.infoq.com/articles/apache-tez-saha-murthy/

Source Code Reading Guides

  • Tez source reading notes by @oza: https://gist.github.com/oza/470e961ff10b60778772
  • BUILDING.txt in the repo: Module structure and build instructions

Simpler Alternatives (GitHub)

  • Luigi: https://github.com/spotify/luigi
  • Dask: https://github.com/dask/dask
  • Prefect: https://github.com/PrefectHQ/prefect
  • pydags: https://github.com/DavidTorpey/pydags
  • simple-dag: https://github.com/leokster/simple_dag
  • Dagu: https://github.com/dagucloud/dagu
  • Ray: https://github.com/ray-project/ray

Source: AI Tools · ai_tools.md · updated 2026-05-19 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

AI Productivity Tips & Tools

A curated list of creative tools, workflows, and ideas to supercharge how you use AI.

Input & Capture

  • Whisper-based dictation (Superwhisper, MacWhisper, Wispr Flow) — way better than macOS native, works system-wide via hotkey
  • Text expanders (Espanso, Raycast, Alfred, aText) — save long prompts as snippets like ;review or ;email
  • Karabiner-Elements — remap a key (like Caps Lock) to instantly trigger your AI tool
  • Clipboard managers (Raycast, Paste, Maccy) — paste previous prompts/outputs without losing them
  • Voice memos → AI — record rambling thoughts on a walk, transcribe, ask AI to organize
  • Screenshot → AI — drag any screenshot in to ask about errors, charts, UIs, handwriting
  • Apple Watch dictation → notes app → AI later — capture ideas anywhere
  • Hotkey to send selected text to AI — most launcher apps support this

Launchers & Integration

  • Raycast AI — invoke AI from anywhere on Mac with a hotkey, no app-switching
  • Keyboard Maestro / Hammerspoon — automate any repetitive Mac task and chain AI calls into them
  • Shortcuts app — build "share sheet" actions that send selected text to AI for summarizing, translating, rewriting
  • AI browser extensions — summarize any page, ask questions about it
  • Reader-mode → AI pipeline — clean article, then summarize
  • Multi-monitor workflow — keep AI chat always open on a side screen
  • Split-screen editing — draft in one window, AI critiques in another

Research & Learning

  • NotebookLM — dump 20 PDFs in, ask questions with citations, no hallucinations beyond sources
  • Perplexity — for research that needs current info with citations
  • YouTube transcripts → AI — get the gist without watching
  • Podcast summaries — Snipd, Airchat, or transcript → AI
  • Reading mode — paste a dense article, ask for ELI5 or for the 3 key takeaways
  • Learning loops — ask AI to quiz you on a topic after explaining it
  • Flashcard generation — feed notes, get Anki-ready cards
  • Spaced repetition prompts — schedule AI to quiz you on past learnings
  • Book / movie summaries — decide if it's worth your time
  • Reading list curator — describe interests, get recommendations with reasoning

Coding & Development

  • Claude Code / Cursor / Windsurf — AI in your terminal/editor instead of copy-pasting
  • Code explainer — paste any unfamiliar code, get a walkthrough
  • Codebase Q&A — tools like Cursor or Claude Code can answer "where is X defined?"
  • Documentation writer — point AI at code, get docs
  • Test generator — paste a function, get unit tests
  • Boilerplate killer — describe what you need, skip the scaffolding
  • AI for regex / shell commands / SQL — describe what you want in plain English
  • Error message decoder — paste any stack trace, get plain-English explanation
  • Git commit messages — paste diff, get a clean commit message
  • PR descriptions — same idea, from diff to description
  • API exploration — paste docs, ask "how do I do X?"
  • JSON / YAML / config translators — paste one format, get another
  • Bulk file renaming / sorting — describe the pattern, get a script

Context Management

  • Personal context doc — keep an "about me" file (role, projects, style) to paste at start of chats
  • Project-specific context docs — one per ongoing project, paste when relevant
  • Prompt library — Notion/Obsidian page with your best prompts, organized by use case
  • Custom GPTs / Projects / Claude Projects — pre-loaded contexts for recurring tasks
  • Memory / persistent context features — let AI remember preferences across chats

Workflow Patterns

  • AI as a rubber duck — explain your stuck problem out loud, AI responds, often unblocks you
  • "What am I missing?" prompts — ask AI what an expert would push back on
  • Pre-mortems — describe a plan, ask AI what could go wrong before you commit
  • Draft → critique → revise loop — iterate instead of trying to one-shot
  • Two-AI workflow — use one AI to critique another's output
  • AI as second opinion — before any important send, paste it and ask "anything I'm missing?"
  • Decision frameworks — describe a choice, ask AI to lay out pros/cons or apply a framework

Writing & Communication

  • Tone shifter — paste blunt draft, ask for "diplomatic version" or "casual version"
  • Length compressor — "make this 50% shorter without losing key points"
  • Length expander — "turn these bullets into a full paragraph in my voice"
  • Refactoring prose — paste anything you wrote, ask for tightening
  • Translation in real-time — paste any foreign text, get instant context
  • AI for naming things — files, variables, projects, products — endless options instantly
  • AI for emoji / formatting — never hunt for the right emoji again
  • Markdown → anywhere — write in markdown, AI converts to whatever format you need

Email & Messaging

  • Email triage — paste inbox subjects, ask AI to rank by urgency or draft replies
  • Cold email drafting — paste recipient's bio, get personalized opener
  • Slack / email tone-check — paste before sending, catch misreads
  • Out-of-office / auto-reply drafting — context-specific responses
  • Saying no gracefully — paste request, get polite decline
  • Difficult conversation rehearsal — role-play with AI before the real talk
  • Feedback drafting — turn harsh thoughts into constructive feedback
  • Negotiation prep — describe situation, ask for likely counterarguments

Daily Productivity

  • Morning planning — dump your todo list, ask AI to prioritize and time-block it
  • End-of-day brain dump — dictate everything you did, ask AI to format it as a status update
  • Calendar prep — paste your day's meetings, ask for prep notes and questions to ask
  • Calendar invite descriptions — turn vague meeting into a clear agenda
  • AI for meeting notes — Granola, Otter, Fireflies record and summarize automatically
  • Onboarding docs — generate from your codebase or process notes

Career & Professional

  • Resume / LinkedIn tailoring — paste job description, get tailored bullets
  • Cover letter starter — never write one from scratch again

Data & Spreadsheets

  • AI for spreadsheets — describe the formula you want in English
  • Data cleaning — paste messy CSV rows, ask AI to standardize
  • Chart suggestions — describe your data, ask what visualization fits best

Visuals & Design

  • Image generation for placeholders, mockups, slide visuals (Midjourney, DALL-E, Ideogram)
  • Image editing via prompt — Photoshop's generative fill, Canva's AI tools
  • Background removal / upscaling — single-purpose AI tools beat manual editing
  • AI presentation builders — Gamma, Tome for first-draft decks

Life Admin

  • Travel planning — itinerary drafts in seconds, then refine
  • Recipe adapter — "I have X, Y, Z in the fridge, what can I make?"
  • Grocery list generator — from a week of planned meals
  • Workout plans — describe equipment and goals, get a routine
  • Habit tracking prompts — daily check-in via AI chat
  • Journaling prompts — AI asks you reflective questions

Mindset

  • Voice + dictation combo — talk while walking, edit when you sit down
  • The compounding rule — every time something feels tedious, pause and ask if AI could do it

c_linked_list

Source: c_linked_list · updated 2024-08-19 · public gist

Synced verbatim from gist.github.com/bl9.

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
  int data;
  struct Node* next;
} Node;

typedef struct Stack {
  Node* head;
} Stack;

void push(Stack *stack, int val) {
  Node *newNode = (Node *)malloc(sizeof(Node));
  newNode->data = val;
  newNode->next = stack->head;
  stack->head = newNode;
}

void pop(Stack *stack) {
  Node *current = stack->head;
  stack->head = current->next;
}

void printStack(Stack *stack) {
  Node *current = stack->head;
  while (current != NULL) {
    printf("%d\n", current->data);
    current = current->next;
  }
}

void initStack(Stack *stack) { stack->head = NULL; }

int main() {
  Stack s;
  initStack(&s);
  push(&s, 1);
  push(&s, 2);
  push(&s, 3);
  push(&s, 4);
  printStack(&s);
  pop(&s);
  printf("------------\n");
  printStack(&s);
  printf("------------\n");
  push(&s, 5);
  printStack(&s);
  return 0;
}