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.
| Updated | Gist | File(s) | Size |
|---|---|---|---|
| 2026-07-25 | bmad-rcon-howto 🔒 | bmad-rcon-howto.md | 42 KB |
| 2026-07-25 | llm 🔒 | llm.md | 70 KB |
| 2026-07-25 | BMAD Recon 🔒 | bmad-recon.md | 99 KB |
| 2026-07-25 | bmad-rcon-foundation 🔒 | bmad-rcon-foundation.md | 43 KB |
| 2026-07-23 | bmad_Research 🔒 | bmad_research.md | 65 KB |
| 2026-07-23 | Similarity-Calculations 🔒 | Similarity-Calculations.md | 33 KB |
| 2026-07-10 | vector_plot 🔒 | vector_plot.md | 4 KB |
| 2026-06-22 | ios_sensors 🔒 | ios_sensors.md | 86 KB |
| 2026-06-11 | autocmds 🔒 | autocmds.lua | 4 KB |
| 2026-06-03 | skull 🔒 | skull.md | 9 KB |
| 2026-06-03 | tez 🔒 | prompt.txt, tez.txt | 73 KB |
| 2026-05-29 | apache_tez 🔒 | apache_tez.md | 36 KB |
| 2026-05-19 | AI Tools 🔒 | ai_tools.md | 8 KB |
| 2024-08-19 | c_linked_list | c_linked_list.c | 872 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 gistSynced 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
- 1. Mode: Draft
- 2. Mode: Process
- 3. Mode: Run
- 4. Type and pack
- 5. Decision shape: explore vs select
- 6. Dimension
- 7. Topology
- 8. The plan gate, line by line
- 9. Effort presets and budgets
- 10. Rounds and leads
- 11. The firewall
- 12. Verification and red team
- 13. Claims and statuses
- 14. Briefs, digests, memlog
- 15. Staleness, refresh, deepen
- 16. The run folder and slug
- 17. Headless and scripting
- 18. Configuration in practice
- 19. Recovering when things go wrong
- 20. All of this in plain Claude, without BMAD
- 21. One-page cheat card
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:
| Check | Why | Fix if missing |
|---|---|---|
| Are your numbers in it? | Volume and pairs change the ranking completely. A generic prompt gets a generic ranking | Add them by hand |
| Is the citation demand hard? | A soft demand produces a soft report | Strengthen it: "every claim, no exceptions" |
| Is the freshness bar stated? | Pricing from 2024 is fiction | Add "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 question | Type | Why not the other one |
|---|---|---|
| "What do DeepL and Google Translate actually offer?" | competitive | Named rivals, teardown shape |
| "How does the localization industry handle post-editing?" | domain | Learning a field's structure and practice, not comparing vendors |
| "Which embedding model should we use?" | technical | Evaluating a technology area for implementation |
| "Does EMA-teacher SSL beat contrastive for retrieval?" | academic-lit | The evidence base is papers |
| "What do users hate about recommendation feeds?" | user-voice | Lived experience, reviews, communities |
| "How big is the legal-tech translation market?" | market | Sizing 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:
- 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.
- Is anything here not load-bearing? If a dimension's answer wouldn't change your choice, delete it.
- 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
| Signal | What it means | What to say |
|---|---|---|
| Breadth-first proposed, but your dimensions all restate one question | The decision was framed too narrowly to fan out | "This is one question — go depth-first" |
| Depth-first proposed for six separable dimensions | The framing collapsed things that should be parallel | "These are independent — breadth-first" |
| Six assistants for something you could Google | Over-provisioned | "Straightforward, quick" |
| Estimate is 50+ minutes for a decision you'll revisit next sprint | Effort/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
| # | Check | Bad answer looks like | What 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 you | Delete / 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
| Preset | Assistants | Sources/round | Rounds |
|---|---|---|---|
quick | 2 | 5 | 1 |
standard | 3 | 8 | 2 |
deep | 6 | 12 | 3 |
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 cost | Preset |
|---|---|
| Undo next sprint | quick |
| Weeks of work to undo | standard |
| Months, or contractual, or regulatory | deep + 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:
- The claims are about different conditions (usually — and the conditions are the real finding)
- One source is wrong (which tells you about that source)
- 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
| Level | What it does | Use for |
|---|---|---|
normal | Spot-checks the claims the recommendation rests on | Most runs |
high | Cross-checks the pack's critical claim classes; red-teams major conclusions | Load-bearing decisions |
max | Checks everything; full-breadth red-team | Regulatory, 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: unverified → verified → disputed → overturned.
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
| Status | Meaning | Your move |
|---|---|---|
unverified | Found, not checked | Fine for background. Never let it be load-bearing |
verified | Spot-checked against another source | Usable |
disputed | Credible sources disagree | The disagreement is the finding. Understand the conditions before choosing |
overturned | Superseded by newer evidence | Kept 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
| Situation | Command |
|---|---|
| Months have passed | refresh |
| A dimension was in scope but thin | deepen |
| A genuinely new question | new run |
| The decision itself changed | new 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
| File | Scope | Wins? |
|---|---|---|
_bmad/custom/bmad-deep-recon.user.toml | Personal, gitignored | Yes |
_bmad/custom/bmad-deep-recon.toml | Team, committed | Middle |
The skill's customize.toml | Defaults | Lowest |
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
| Symptom | Cause | Fix |
|---|---|---|
| Report reads like a textbook | No decision stated → nothing to prune against | Restate as a decision with constraints; re-run |
| Report agrees with everything you believed | You pasted your design in | Re-run without it, compare afterward yourself |
| Matrix weights feel generic | Constraints weren't stated as constraints | Say them explicitly at the gate; deepen or re-run |
| A dimension came back thin | Brief too broad or too narrow | cat the brief to diagnose, then deepen with a sharper framing |
| Everything traces to two aggregator sites | Source policy too permissive | Ban the kind; re-run the affected dimension |
| Run died partway | Context or session loss | Resume — files-first means what landed is kept |
| Took 3x the estimate | Preset too high, or too many dimensions | Cut dimensions first, preset second |
| You disagree with the recommendation | Either your weights were wrong, or your prior was | Fix 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:
| Feature | Replicable 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 gistSynced 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
- 0. The one thing to unlearn first
- 1. Concrete anchor: a real model's dimensions
- 2. Tokenization
- 3. The residual stream
- 4. Attention, dissected properly
- 5. The MLP — where knowledge is stored
- 6. Superposition
- 7. Circuits — worked examples
- 8. Depth = composition, and where intent lives
- 9. From vector to token: the output side
Part II — Putting it together
- 10. Full worked trace
- 11. Why prediction produces intent modeling
- 12. In-context learning
- 13. Chain of thought = serial compute
Part III — How it got that way
Part IV — Applied
- 15. Worked scenario: a landing page
- 16. Worked scenario: a compiler pass
- 17. Failure modes
- 18. What is honestly still unknown
Part V — Reference
- 19. Every concept, defined and modeled
- Appendix A: the compressed mental model
- Appendix B: the analogy that actually holds
- Further reading
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 model | Transformer LM | |
|---|---|---|
| Output per input | One vector for the whole text | One vector per token position |
| Vector meaning | A point in a static semantic space | A running computation, mid-flight |
| How many times written | Once | ~80–120 times (once per layer) |
| Information flow | None between docs | Every position reads from every earlier position, at every layer |
| Nature | A noun — a stored representation | A 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:
- 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. - 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:
- The corpus is your own prompt, not a static index.
- 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.
- 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 type | Job | Observed in |
|---|---|---|
| Previous-token head | Attend to position i−1 | Every model, layer 1–2 |
| Duplicate-token head | Find earlier copies of the current token | Early-mid layers |
| Induction head | Pattern [A][B] … [A] → predict [B] | Layers ~2+ |
| Name-mover head | Copy a specific name to the output position | Mid-late |
| Syntax heads | Subject→verb, noun→adjective, bracket matching | Early-mid |
| Suppression heads | Actively reduce a candidate's probability | Late |
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_inis a key: a direction in stream-space it detects. - Each corresponding column of
W_outis 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:
- 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."
- 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
zqemits a query: "where else have I seenzq?" - Duplicate-token / previous-token machinery has tagged the earlier occurrence.
- The induction head attends to the token after the earlier
zq— which isBuffer. - OV circuit copies
Bufferinto 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:
| Group | Function |
|---|---|
| Duplicate-token heads | Detect that John appeared twice |
| S-inhibition heads | Write a signal that suppresses attention to John |
| Name-mover heads | Attend 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:
| Layers | What forms |
|---|---|
| 0–5 | Detokenization: reassembling word pieces, resolving word sense, basic syntax |
| 5–25 | Phrases, entities, grammatical relations, factual lookups |
| 25–55 | Abstract task representation, relationships between entities, register and intent, plan formation |
| 55–75 | Concretizing the plan into specific content |
| 75–80 | Converting 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:
- Unembed:
logits = W_U · x→ 128,256 raw scores. - Softmax with temperature:
p = softmax(logits / T). T=0 → always the argmax (deterministic). T=1 → the raw learned distribution. T>1 → flatter, more random. - 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.
- Sample one token.
- 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
| Depth | What accumulates in the stream |
|---|---|
| L1–4 | Token pieces reassembled. " cache" is disambiguated — CPU cache? cache-money? Attention to " tests", " layer" resolves it toward software caching. |
| L5–12 | Syntax: 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–25 | Domain 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–40 | Discourse-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–60 | Response 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–75 | Concretization. Abstract "shared state leaks across tests" becomes lexical material — words like isolation, teardown, fixture, singleton gain probability mass. |
| L75–80 | Projection 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:
- 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:
| Fragment | Origin |
|---|---|
| "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-leak → teardown 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:
- 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.
- 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:
resultwas 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
letbindings - 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 training | 4K–32K, extended later |
| Total FLOPs | ~6 × params × tokens ≈ 6×10²⁴ |
| Hardware | 10⁴–10⁵ accelerators, weeks to months |
| Cost | tens to hundreds of millions of dollars |
| Optimizer | AdamW — 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
| Stage | Data | What it adds | What it doesn't |
|---|---|---|---|
| Pretraining | ~15T tokens of web, books, code | Essentially all knowledge, reasoning ability, world model, intent modeling | Any notion of being an assistant. A base model given a question is as likely to write more questions. |
| SFT | ~10⁴–10⁶ curated dialogues | The assistant format; the mapping "user turn → helpful response" | New knowledge, in any meaningful quantity |
| Reward modeling | Human preference comparisons (A vs. B) | A learned scorer that approximates human judgment | Anything, directly — it's scaffolding for the next stage |
| RLHF / RLAIF | The reward model's scores | Tone, helpfulness, honesty, refusal behavior, calibrated hedging | New capabilities |
| RLVR | Math/code with automatic checking | Genuine reasoning improvement | Anything 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 stage | Can fail? | Transformer equivalent | Can fail? |
|---|---|---|---|
| Lexer | yes — invalid character | BPE tokenizer | no — total function, always emits tokens |
| Parser | yes — syntax error | (none exists) | — |
| Type checker | yes — type error | (none exists) | — |
| Linker | yes — undefined symbol | (none exists) | — |
| Codegen | yes | softmax | no — 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-pageschema: hero, value proposition, social proof, pricing, CTA, footerlocal-service-businessschema: 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:
- L25–45: composes
landing-page∩local-service∩pet-industryinto a joint plan. Pet industry contributes warm colors, photography-forward, informal-but-trustworthy tone. - L45–65: instantiates concrete sections in order.
- 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. - 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.
| Failure | Root cause |
|---|---|
| Can't count letters in "strawberry" | Tokenization — it never sees characters |
| Arithmetic on large numbers | Fixed 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 contexts | Attention mass is finite and spread over more positions; "lost in the middle" is real |
| Repetition loops | Low-temperature sampling entering a self-reinforcing attractor |
| Fails at genuinely novel deep reasoning | No search, no backtracking. Layer-by-layer refinement isn't tree search |
| Reverses its position under pushback | Post-training partially selected for agreeableness — Goodharting on human preference |
| Cites a real-looking but nonexistent paper | Author, 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
| Term | What it literally is | Mental model |
|---|---|---|
| Parameter | One 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. |
| Weight | A parameter used as a multiplier on an input. The overwhelming majority of parameters. | How strongly one thing influences another. Learned "conductance" in a circuit. |
| Bias | A 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 matrix | A 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. |
| Activation | A 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. |
| Neuron | One 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). |
| Tensor | An 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 multiplication | The 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_model | The 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. |
| Precision | Bits 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
| Term | What it literally is | Mental model |
|---|---|---|
| Token | A 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. |
| Vocabulary | The full token set, ~128K entries. | The model's fixed symbol table, built once by BPE before training. |
| BPE | Byte-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 matrix | vocab × 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 / RoPE | Rotates 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 / block | One attention sublayer + one MLP sublayer. 80 of them stacked. | One optimization pass — except nobody wrote it. Each reads the workspace and writes back. |
| Residual stream | The running vector at each position that every sublayer adds into. | The shared whiteboard. The single most important object. Not overwritten, only accumulated. |
| Skip / residual connection | x ← 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. |
| Attention | softmax(QKᵀ/√d)V | Learned 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 head | One 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 pattern | The 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 mask | Position i can't attend to j > i. | Enforces left-to-right. Also why one document yields thousands of training signals in one pass. |
| GQA / MQA | Multiple query heads sharing fewer key/value heads. | A KV-cache size optimization. Pure engineering; almost no capability cost. |
| MLP / FFN | W_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 function | GELU, 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 / RMSNorm | Rescale 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 head | d_model × vocab matrix at the very end. | Projects the final vector onto every possible token. Often tied to the embedding matrix. |
| Logits | Raw pre-softmax scores, one per vocab entry. | Unnormalized evidence for each token. Differences matter; absolute values don't. |
| Context window | Max 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
| Term | What it literally is | Mental model |
|---|---|---|
| Loss | −ln P(actual next token). | "How surprised were you?" Zero means certain and right. That's the entire objective. |
| Cross-entropy | The formal name for that loss. | Measures the gap between the model's distribution and reality. |
| Perplexity | exp(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. |
| Backpropagation | The 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. |
| SGD | Take 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 / AdamW | SGD 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 rate | Step size multiplier, ~1e-4, decaying. | How far to move per step. Too high: divergence. Too low: never arrives. The most important hyperparameter. |
| Warmup + decay | Ramp 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). |
| Batch | The 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 accumulation | Sum gradients over several forward passes before stepping. | Simulating a huge batch when it won't fit in memory. |
| Epoch | One 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. |
| Checkpoint | A saved snapshot of all weights. | A build artifact. What actually gets shipped. |
| Overfitting | Memorizing 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 laws | Loss 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. |
| Emergence | A 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. |
| Grokking | Sudden 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. |
| Pretraining | Next-token prediction on ~15T tokens. | Where all knowledge and reasoning comes from. 98%+ of the compute. |
| SFT | Supervised 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 model | A model trained on human A/B preferences to score outputs. | A learned approximation of human taste. Scaffolding for RLHF. |
| RLHF / RLAIF | RL against the reward model's scores. | Manners, tone, refusals, calibrated hedging. Adds almost no capability. Optimizing approval, not truth — hence sycophancy. |
| RLVR | RL 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. |
| Distillation | Training 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 / PEFT | Train 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 forgetting | Fine-tuning on narrow data degrades general ability. | Overwriting shared circuits. The reason naive fine-tuning often makes a model worse overall. |
19.4 Inference
| Term | What it literally is | Mental model |
|---|---|---|
| Forward pass | Input → 80 layers → logits. No weights change. | Running the compiled program. Same weights every time; the data flow is what your prompt reconfigures. |
| Prefill | Processing your whole prompt in one parallel pass. | The compile step before output starts. Compute-bound. This is your time-to-first-token. |
| Decode | Generating 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 cache | Stored 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. |
| Sampling | Drawing a token from the output distribution. | The one deliberately nondeterministic step. Why the same prompt gives different answers. |
| Temperature | Divide 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-p | Keep 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 decoding | Always take the highest-probability token. | Deterministic, and often worse — it produces flat, repetitive text and can dead-end into loops. |
| Beam search | Track multiple candidate continuations. | Standard in translation, mostly abandoned for open generation — it collapses into bland high-probability text. |
| Stop sequence | A string that halts generation. | An explicit terminator, since the model has no intrinsic sense of "done" beyond a learned end-of-turn token. |
| Prompt | Everything 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 prompt | Instructions 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-shot | No examples / a handful of examples in the prompt. | Few-shot works via induction-head machinery — pattern completion, not learning. Weights never move. |
| In-context learning | Acquiring a task from the prompt alone. | The forward pass implementing a learning algorithm. Sometimes literally approximating gradient descent inside the attention layers. |
| Chain of thought | Generating intermediate reasoning tokens. | Renting serial compute. Fixed 80-layer depth × N tokens generated. The context window becomes external working memory. |
| Speculative decoding | A 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. |
| Quantization | Dropping to int8/int4 at inference. | Shrinking the binary. Big memory and speed wins, small quality cost. |
19.5 Interpretability
| Term | What it literally is | Mental model |
|---|---|---|
| Feature | A 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." |
| Superposition | Storing 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. |
| Polysemantic | One neuron firing for several unrelated things. | The symptom of superposition. Why reading individual neurons fails. |
| Monosemantic | One 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. |
| Circuit | A set of components implementing a specific algorithm. | A discovered subroutine. Induction heads, the IOI circuit. Real algorithms nobody wrote. |
| Induction head | A two-layer circuit doing [A][B]…[A] → [B]. | Why in-context learning works. Also why the model tracks variable names it has never seen. |
| Ablation | Zeroing a component and measuring the damage. | #ifdef 0 on part of the network. How circuit claims get causally verified rather than just observed. |
| Activation patching | Copying activations from one run into another. | A/B testing internals. Isolates which component carries which information. |
| Logit lens | Applying the output head to intermediate layers. | Reading the model's mind mid-computation. Shows the answer being built, not looked up. |
| Task / function vector | A 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
| Term | What it literally is | Mental model |
|---|---|---|
| FLOPs | Floating-point operations. | The currency. Inference ≈ 2 × params per token. Training ≈ 6 × params × tokens. Everything else is bookkeeping. |
| TTFT | Time to first token. | Dominated by prefill — i.e. by prompt length. Long prompts cost you latency before a single word appears. |
| Tokens/sec | Decode throughput. | Bounded by memory bandwidth, not FLOPs. Batching many requests together is nearly free on compute. |
| Throughput vs latency | Total tokens across all users vs. speed for one. | Directly in tension. Big batches maximize throughput and hurt individual latency. |
| RAG | Retrieve 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. RAG | Three 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. cost | Cost is superlinear-ish in context. | Attention is O(n²) in compute, KV cache is O(n) in memory. The memory usually bites first. |
| Determinism | Same 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
| Term | What it literally is | Mental model |
|---|---|---|
| Hallucination | Fluent, 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. |
| Sycophancy | Caving 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. |
| Goodharting | Optimizing 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 middle | Degraded recall of mid-context information. | Attention mass is finite and softmax-normalized. More positions, less each. Beginnings and ends survive best. |
| Repetition loop | The model cycling the same phrase. | Low-temperature sampling entering a self-reinforcing attractor — each repeat raises the probability of the next. |
| Prompt injection | Untrusted 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. |
| Jailbreak | Prompting 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 cutoff | The date training data ends. | The build date. Everything after is invisible unless supplied in context. |
Appendix A: the compressed mental model
- Your text becomes tokens; tokens become vectors.
- Those vectors are a shared workspace, one column per position, and every layer adds to rather than replaces them.
- Attention moves information sideways — learned retrieval, recomputed 5,120 times per token, where "what to match" and "what to copy" are separate learned functions.
- MLPs add knowledge vertically — ~2.3 million learned key-value slots.
- Each vector holds a sparse combination of millions of learned features, packed via near-orthogonality. Width was never the constraint.
- Depth is composition. By mid-network, your intent exists as an extractable vector, independent of your wording.
- The final vector projects to a distribution over 128K tokens; one is sampled; the loop runs again.
- It knows what you meant because predicting text written by intentional agents requires modeling intent, and compression made that the cheapest available strategy.
- Generating tokens buys serial compute; that's what chain of thought is for.
- 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 gistSynced 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. Thenbmad-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?"
- 1. Background: BMAD concepts you need first
- 2. What PR #2611 actually changed
- 3. The three modes
- 4. Research types (the "packs")
- 5. How a native Run thinks
- 6. Why the reports hold up
- 7. The run folder, and research as a living asset
- 8. Where it sits in the BMAD lifecycle
- 9. Configuration
- 10. Research checkpoints across a feature's life
- 11. The full use-case catalog
- 12. Applied: a user recommendation feature
- 13. Deep dive: privacy and regulatory research
- 14. Deep dive: security and adversarial research
- 15. Deep dive: algorithm and technique-trend research
- 16. Applied: a multilingual translation feature
- 17. Deep dive: licensing, procurement and vendor risk
- 18. Best practices
- 19. Anti-patterns
- 20. Command cheat sheet
- 21. Transferable design lessons
- 22. The epistemology underneath all of this
- 23. Worked walkthroughs for software engineers
- Walkthrough A — Native Run: choosing an embedding model
- Walkthrough B — Draft → Process: the MT vendor landscape
- Walkthrough C — The compliance gate, done without fooling yourself
- Walkthrough D — Threat-modeling a ranking surface
- Walkthrough E — Refresh, three months later
- Walkthrough F — Deepen one dimension
- Walkthrough G — Headless refresh in CI
- Walkthrough H — The anti-walkthrough
- 23.9 Team integration patterns
- 23.10 Calibration — what to spend where
- 24. Sources
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:
| Feature | Deep Recon value | Why |
|---|---|---|
| User recommendation (in an existing OpenSearch/HNSW stack) | High — but not where you'd guess | Index 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 translation | High | Vendor/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
| Term | Meaning |
|---|---|
| Skill | A directory containing SKILL.md (the entry point) plus supporting files. The unit of installation and invocation. Invoked as /bmad-<name>. |
SKILL.md | The router/entry file. Modern BMAD keeps it lean and defers detail to references/. Every token in SKILL.md is paid on every invocation. |
references/*.md | Procedure files loaded just-in-time, only when that branch is taken. This is the token-optimization pattern. |
customize.toml | Per-skill config. Merged through a layered resolver (see §9). |
| Agent | A skill with a persona (e.g. Mary the Analyst). Agents expose a menu of options that route to workflow skills. |
| Workflow | A multi-step skill. Older ones sharded into step-01…step-NN.md; newer ones use mode-based reference files. |
| Core skill vs module skill | src/core-skills/ ships to every install. src/bmm-skills/ ships only with the BMM (software dev) module. |
| Shim | A stub skill that forwards an old ID to a new one so existing habits don't break. |
| Subagent / assistant | A spawned sub-context with its own token budget, given a narrow brief, returning a digest. |
| Planning artifacts | The output folder where analysis/planning docs land, consumed by downstream skills. |
| Memlog | An 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:
- Hallucinated grounding. Ask a model about market size or vendor pricing and it answers from training data — confidently, uncitably, and often 18 months stale.
- 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.mdfrom ~3,989 to ~2,091 tokens by carving procedures intoreferences/.
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:
| Finding | Fix |
|---|---|
{brief} interpolated inside a double-quoted shell template — $(), backticks, and quote-breaking could reach a command line, amplified by --dangerously-skip-permissions | Contract 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 unvalidated | Allowlist to http(s) only; escape source-derived text before insertion |
| Refresh/Deepen reset verification status for out-of-scope claims | Statuses preserved outside the refresh scope |
user-voice pack ranked community posts above surveys | Triangulate instead; redact usernames, handles, emails, identifying URLs from verbatim quotes |
Parallel dimension writes raced on research.md | Explicit single-writer rule: digests return to the lead, which alone writes in plan order |
3. The three modes
| Mode | What happens | You supply |
|---|---|---|
| Draft | Composes a research prompt carrying the type pack's craft — pruned dimensions, freshness bars, source policy, a hard citation demand — tuned to the tool you name | One paste into ChatGPT / Gemini / Grok / Perplexity |
| Process | Files 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 frontmatter | The report, from any source |
| Run | Native research in session: plan gate → parallel fan-out → verification at landing → cited synthesis | Approval at one plan gate |
Choosing
| Situation | Mode |
|---|---|
| You already pay for a deep-research product and don't mind one round-trip | Draft → Process |
| You already have a report (analyst PDF, colleague's doc, whatever) | Process |
| You want results now, one sitting, no app switching | Run |
| Research needs internal sources or MCP tools only your session can reach | Run |
| Broad public sweep first, targeted follow-up second | Draft + 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.
| Type | Use when |
|---|---|
market | Sizing an opportunity, segments, pricing, go-to-market |
domain | Learning an industry or field: structure, players, rules, vocabulary |
technical | Evaluating a technology area, integration approaches, implementation reality |
competitive | Tearing down named competitors: offers, pricing, trajectory, sentiment |
user-voice | What users actually experience and want: reviews, communities |
academic-lit | Literature 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
| Preset | Assistants | Sources / round | Rounds |
|---|---|---|---|
quick | 2 | 5 | 1 |
standard (default) | 3 | 8 | 2 |
deep | 6 | 12 | 3 |
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
- 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.
- The research firewall. Your project files and briefs shape what gets asked, never what gets found. Assistants receive only their assignment.
persistent_factsdefaults 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
| Level | Behavior |
|---|---|
normal (default) | Spot-checks the claims the recommendation actually rests on |
high | Cross-checks the pack's critical claim classes and red-teams major conclusions |
max | Checks 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:
_bmad/custom/{skill-name}.user.toml— personal, gitignored — wins_bmad/custom/{skill-name}.toml— team/org, committed- 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
| Key | Purpose |
|---|---|
validation | normal / high / max |
red_team | Adversarial pass; default off |
use_workflows | Whether to invoke other workflows |
subagent_models | Which model each assistant class uses |
output_format | auto / html / md / both |
external_sources | MCP research sources — shipped examples: Tavily, Perplexity Sonar, xAI X-Search |
| source policies | Preferred and banned sources |
| doc standards, handoffs | Downstream contract |
| custom types | Add 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.
| # | Gate | The question | Type + shape | Cost of skipping |
|---|---|---|---|---|
| 0 | Validate | Do users actually want this? What already exists? | user-voice, competitive | You build something nobody asked for |
| 1 | Define success | How is this measured, and does the metric predict anything real? | academic-lit | You optimize a number that doesn't move the business |
| 2 | Build vs. buy | Is there a vendor? What's the exit cost? | competitive + select | Eighteen months rebuilding a commodity |
| 3 | Legal to ship? | Lawful basis, disclosure, residency, retention, transparency duties | domain | Redesign after legal review, or a regulator finds it |
| 4 | Attack surface | Abuse vectors, adversarial ML, supply chain | technical | Security review blocks launch; or it doesn't and you learn later |
| 5 | Licensing & procurement | Can we actually use this model/library/vendor? | domain / competitive | Chosen model turns out to be non-commercial; vendor has no DPA |
| 6 | Component selection | Which model, index, library, framework? | technical + select | Reversible, usually — the cheapest gate to get wrong |
| 7 | Failure modes | What broke for everyone else who shipped this? | technical | You rediscover known incidents in production |
| 8 | Cost at scale | What does this cost at 100× current volume? | technical | Unit economics collapse post-launch |
| 9 | Refresh | Did the ground move under a decision we already made? | refresh | Silent 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 case | Type + shape | Run it when | Typical miss if skipped |
|---|---|---|---|---|
| 1 | Feature validation | user-voice | Before committing a quarter to a surface nobody complained about | Building a feature that solves a problem users don't have |
| 2 | Competitive teardown | competitive | A rival shipped the thing you're about to ship | Reinventing, or missing table-stakes behavior |
| 3 | Metric & eval methodology | academic-lit | Your metric will govern every future decision on the feature | Optimizing an offline metric with no online correlation |
| 4 | Component / model selection | technical + select | Switching cost is high (index format, embedding dim, framework) | Lock-in to a dying option |
| 5 | Build vs. buy | competitive + select | A credible vendor exists | Rebuilding a commodity, or buying an unexitable dependency |
| 6 | Privacy & data protection | domain | Any personal data, profiling, or automated decisioning — see §13 | Redesign after legal review; regulatory exposure |
| 7 | Security, abuse & adversarial ML | technical | The feature is user-facing, user-influenced, or model-driven — see §14 | Shipping a manipulable ranking surface |
| 8 | Licensing & IP | domain | Any third-party model, dataset, or copyleft library — see §17 | Discovering the model is non-commercial after integration |
| 9 | Procurement & vendor assurance | competitive | Vendor will process your data — see §17 | Legal blocks a vendor you already built against |
| 10 | Standards & interop | technical | Language tags, encodings, collation, protocols, schemas | Subtly wrong behavior in locales you don't test |
| 11 | Accessibility & inclusive design | technical / domain | New UI surface, especially multilingual | WCAG failures found at audit; RTL and text-expansion breakage |
| 12 | Fairness, bias & transparency duties | academic-lit + domain | Ranking or recommending to people at scale | Popularity-bias spirals; unmet transparency obligations |
| 13 | Algorithm & technique trends | academic-lit | You suspect the state of the art moved — see §15 | Building on a technique the field has quietly abandoned |
| 14 | Failure-mode / postmortem research | technical | Before designing anything with a known-hard failure mode | Rediscovering others' incidents in your own production |
| 15 | Cost modeling at scale | technical | Per-request inference, egress, or storage costs dominate | Unit economics that only fail at volume |
| 16 | Dependency health & maintenance risk | technical | Adopting a library or model you'll depend on for years | Adopting an abandoned project with a bus factor of one |
| 17 | Observability & eval tooling | technical + select | You need to measure a system you can't easily inspect | No way to tell whether the feature is working |
| 18 | Migration & deprecation risk | technical | Your current stack component has a sunset signal | Forced 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.mdwith 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
| Decision | Type + shape | Dimensions to insist on at the plan gate |
|---|---|---|
| Embedding model for item/user vectors | technical + select | Multilingual 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 diversity | technical + select | Latency at your p99 budget, quality delta on public rerank benchmarks, serving cost, staleness of published numbers |
| ANN index choice / HNSW parameterization defaults | technical | Recall-vs-latency curves published by index maintainers, memory footprint at your cardinality, filtered-search behavior |
| Offline metric conventions | academic-lit | NDCG@k vs. MRR vs. Recall@k in the recsys literature, position-bias correction, offline↔online correlation evidence |
| Cold-start strategy | academic-lit or domain | Published approaches, reported lift, honest failure reports |
| What users complain about in "recommended for you" surfaces | user-voice | Filter-bubble complaints, repetition, staleness, opacity |
| Lawful basis, retention, erasure, transparency duties | domain | See §13 — the highest-value research on this feature |
| Manipulation, poisoning and inference attacks | technical | See §14 — shilling, embedding inversion, membership inference |
| Popularity bias and fairness obligations | academic-lit + domain | Measured popularity-bias effects, mitigation cost in NDCG, disclosure duties |
| Embedding model / reranker licensing | domain | Field-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 datarule 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
| Knob | Setting | Why |
|---|---|---|
| Source policy — preferred | EUR-Lex, EDPB, national DPAs, ICO, OPC Canada, CAI Québec, court decisions | Primary texts and regulator guidance beat everything |
| Source policy — banned | Law-firm marketing blogs, compliance-vendor content marketing | This space is saturated with SEO'd summaries that misstate scope and are optimized to sell a product |
validation | high minimum | A wrong regulatory claim is load-bearing by definition |
red_team | on (it's off by default) | If you walked in believing you're compliant, buy the adversarial pass |
| Freshness windows | Statute ~36mo · guidance ~12mo · enforcement/case law ~6mo | These claim classes genuinely age at different rates |
| Firewall | Do not paste your retention design in | Otherwise 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
| Knob | Setting |
|---|---|
| Preferred sources | NIST, OWASP, MITRE ATT&CK/ATLAS, CVE/NVD, vendor security advisories, peer-reviewed adversarial-ML venues, credible incident writeups |
| Banned sources | Security-vendor content marketing, "top 10 threats" listicles |
validation | high |
red_team | on — this is the category it was built for |
| Freshness | Attack 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:
- 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.
- 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.
15.2 Why "trends in algorithms like X" is the anti-pattern
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 ask | Decision framing | Type + 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
| Decision | Type + shape | Why external |
|---|---|---|
| MT provider / model selection | competitive + select | Pricing, language pairs, quality claims, and rate limits are all public and all move |
| Quality evaluation methodology | academic-lit | BLEU vs. chrF vs. COMET vs. LLM-as-judge is a live methodological debate; picking wrong invalidates your whole eval |
| Locale, normalization, and collation handling | technical or domain | These are external standards (Unicode/CLDR/ICU), not opinions |
| Human-in-the-loop / post-editing workflow | domain + user-voice | Localization industry has established practice you shouldn't reinvent |
| Data residency and PII in translation payloads | domain | Regulatory, jurisdiction-specific, and changes |
| Terminology/glossary and do-not-translate handling | technical | Provider 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
highcross-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:
- Draft a prompt for whichever deep-research product you subscribe to. Vendor landscapes are exactly what hosted crawlers are good at.
- Run it there; bring the report back.
- Process it — original preserved in
imports/, claims extracted, gaps flagged against thecompetitivepack. - 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
- 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.
- State your constraints as constraints. Latency budget, memory ceiling, language mix, volume, license posture, deploy target. These become matrix weights.
- Name candidates as inclusions, never as the frame. Otherwise you get a confirmation exercise.
- Declare your freshness bar when the field moves fast. Don't accept the pack default silently.
- Pick the decision shape deliberately.
selectfor choose-between,explorewhen you don't yet know the option space. Runningselecttoo early narrows prematurely.
18.2 At the plan gate
- 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.
- 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.
- Match effort to reversibility.
quickfor a decision you can undo next sprint;deep+validation = highfor something that becomes load-bearing. - 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
- 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.
- Chase answer-engine citations to primaries. If the report leans on an aggregator, that's one publisher, not three.
- Read the "could not determine" section first. Gaps are more informative than confirmations.
- Check the staleness map and act on it. Research that's never refreshed is research that silently becomes wrong.
- Keep
research.mdin 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
- 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.
- Default to
standard. The presets were rescaled downward because v1 was too slow and too token-heavy. Reach fordeepdeliberately. - Use
deepeninstead of re-running. Drilling one dimension is far cheaper than a fresh engagement. - Use
refreshinstead of redoing. It re-verifies only what's stale.
18.5 Hygiene
- Turn on
red_teamfor decisions with a strong prior. It's off by default. If you walked in already believing the answer, buy the adversarial pass. - Redact before quoting people. The
user-voicepack now requires stripping usernames, handles, emails, and identifying URLs from verbatim quotes — carry that rule into anything you paste into a PRD. - 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-permissionsflag.
18.6 Domain-specific craft
- Run the compliance and security gates in week one, not week ten. Their findings are design constraints. Discovered late, they're rewrites.
- 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.
- 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.
- Always add a "negative results and failures" dimension to any technique or trend research. Published literature is positive-biased and won't volunteer it.
- 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.
- 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.
- Convert security output into accepted-or-mitigated decisions, with citations attached. A threat list is not a deliverable; a threat register with dispositions is.
- 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-pattern | Why it fails | Do instead |
|---|---|---|
| "Research recommendation systems" | No decision → unbounded dimensions → expensive encyclopedia | Name the decision and constraints |
| Running research on your own codebase | Firewall means it looks outward; your repo isn't a source | bmad-document-project |
| Pasting architecture docs "for context" | Defeats the firewall; produces agreeable findings | Let context shape questions only |
deep + max on every run | Burns tokens and minutes for marginal gain | Scale to reversibility |
| Approving the plan gate without reading | The one place your judgment is irreplaceable | Prune dimensions, check topology |
Treating research.md as a one-time artifact | Claims rot; downstream artifacts inherit the rot | Refresh from the staleness map |
| Using research to decide how to implement | Wrong tool; you need your codebase, not the web | Story workflows |
| Accepting vendor-published quality numbers | Systematically flattering | Flag self-reported; verify at high |
| Naming your preferred option in the framing | Converts 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" run | Three source policies in conflict; shallow on all three | Three engagements, three policies |
| Sourcing compliance claims from law-firm blogs | SEO'd, scope-distorting, sales-motivated | Ban them; require regulators and primary texts |
| Threat list with no dispositions | Reads like diligence, changes nothing | Threat → likelihood/impact → control or accepted, cited |
| Trend research with no negative-results dimension | Aggregates only claimed wins; confidently wrong | Name failed reproductions as a required dimension |
| Researching a parameter your own data can settle | Expensive procrastination | quick for the prior, then measure |
| Vendor research after integration | Discovers blockers when unwinding costs most | Procurement gate before the first API call |
| Assuming open weights means usable | Field-of-use and MAU clauses bite late | Read the license text, not a summary |
20. Command cheat sheet
| Goal | Type 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:
- 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.
- 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.
- Files-first, conversation-as-control-channel. Anything valuable goes to disk on arrival. Contexts die; disks don't.
- Verify at landing, not at the end. A late pass degraded quality in field testing.
- Mechanical work belongs in a tested script. Counting, cross-referencing, date math, slug generation.
- Build an explicit bias firewall. Separate "what shapes the question" from "what counts as evidence," and enforce it at the subagent boundary.
- Choose fan-out topology deliberately. Parallelism is a cost, not a virtue.
- Treat tool output as untrusted input. File-based handoff over shell interpolation, protocol allowlists, escaping at the boundary.
- Give artifacts a lifecycle. Staleness maps, delta refreshes, and downstream-impact warnings turn a document into an asset.
- 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.
- It's true
- You believe it
- You have good reason — justification — 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
| Concept | Plain meaning | Everyday example |
|---|---|---|
| Justification | The 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" |
| Testimony | Believing something because someone told you. Most of what anyone knows works this way — you've never personally verified that Antarctica exists | Trusting a doctor, a map, a news report |
| Foundationalism | The 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 solid | A wall of bricks needs a foundation, not infinitely more bricks below |
| Defeasible | A 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 |
| Defeater | The new evidence that overturns it | The posted schedule change |
| Fallibilism | Accepting that any of your beliefs might be wrong, without concluding that nothing is worth believing. Confidence without certainty | Science's default posture |
| Falsification | Testing an idea by trying hard to break it rather than piling up examples that fit. Associated with Karl Popper | Don't count white swans; go looking for a black one |
| Calibration | Your confidence matching your actual accuracy. Being 70% sure of things that turn out true about 70% of the time | A weather forecaster who says "30% rain" and is right about that often |
| Provenance | Where a claim came from, and through whose hands | A museum tracing an artifact's ownership chain |
| Independence | Two sources counting as genuinely separate evidence, rather than both repeating one original | Two 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.
| Mechanism | What 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 appendix | Foundationalism. 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 claim | The 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 policy | A 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 class | Fallibilism 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 pass | Falsification. 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" section | Calibration. 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 literature | Corrects 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 end | Justification 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 truth | The 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_factsdefaults 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.
-
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.
-
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.
-
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.
-
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.
-
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 concept | Its form in your systems |
|---|---|
| Publication bias | Popularity bias — rich-get-richer feedback loops distorting what's available to be seen |
| Epistemic closure / narrowed inquiry | Filter bubbles — the system narrowing the space of what a user can encounter |
| Transparency of grounds | DSA 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 provenance | Source attribution in retrieval and RAG — whether users can trace what they're being shown |
| Independence of sources | Deduplication and diversity in candidate sets — ten versions of one document are one document |
| Calibration | Confidence display and abstention — showing "we're not sure" instead of always returning ten results |
| The firewall | Personalization 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:
| Element | Why it matters |
|---|---|
| The decision and its horizon | Prunes dimensions. "12 months, expensive to reverse" tells the run to weight license stability and maintenance health, not just today's benchmark |
| Constraints as constraints | These 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 bar | Embedding 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:
- 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
deepsaves roughly 15% of the run. - 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.
- 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.
- Check the estimate against your patience. 40 minutes is real. If you'll wander off at minute 12, drop to
standardnow. 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
- "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.
- 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.
- Self-reported vs. independent numbers. Every vendor-run benchmark should be flagged. If they aren't, ask.
- 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.
- 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:
| Step | Why |
|---|---|
Original preserved untouched in imports/ | Provenance. You can always go back to what the tool actually said |
Claims extracted to digests/ behind the firewall | Your project context can't leak into the extraction |
Gap-check against the competitive pack | The 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 metadata | Downstream 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 here | Impact | Disposition | Artifact |
|---|---|---|---|---|
| Shilling / coordinated profile injection | Medium — accounts are cheap | High — ranking integrity | Mitigate | Story: interaction-rate anomaly detection per item |
| Embedding inversion recovering attributes | Low — vectors not exposed at API | High if exposed | Control | ADR: user vectors never leave the service boundary |
| Cross-user leakage via shared segments | Medium | Medium | Mitigate | Story: minimum cohort size before a segment is surfaced |
| Model extraction via systematic querying | Low — rate limits exist | Medium | Accept, documented | Note in threat register, with citation |
| Feedback-loop poisoning (slow-drip) | Medium | High — hard to detect | Investigate | Spike: 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
| Engagement | Preset | Validation | Red team | Time | Worth it when |
|---|---|---|---|---|---|
| Library or framework pick | quick | normal | off | ~8 min | Multi-year dependency |
| Dependency health check | quick | normal | off | ~6 min | Always. Cheapest useful run there is |
| Metric / eval methodology | standard | normal | off | ~20 min | The metric will govern future decisions |
| Component selection (model, index) | standard–deep | high | off | 25–45 min | Reversal cost is weeks |
| Vendor / build-vs-buy | deep | high | off | 35–50 min | Data leaves your boundary |
| Privacy / regulatory | deep | high | on | 40–60 min | Any personal data or profiling |
| Security / threat model | deep | high | on | 40–60 min | User-facing or model-driven surface |
| Algorithm / technique trend | quick–standard | normal | off | 10–20 min | Only after converting it to a decision |
Two patterns in that table worth naming:
red_teamis 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.mddocs/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 gistSynced 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
- 2. Layer 1 — the AI substrate
- 3. Layer 2 — how BMAD is built
- 4. Layer 3 — Deep Recon's own vocabulary
- 5. Six mental models that carry the most weight
- 6. Where the metaphors break
- 7. Learning path: your first two weeks
- 8. Resources and references
- 9. What I'm confident about, and what I'm not
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:
- A decision — stated up front, with constraints. Everything else is shaped by it.
- A plan you approve — what will be investigated, how, and for how long.
- Parallel lookup with a barrier — several sub-processes search, none of them sees your assumptions.
- 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
| Term | What it is | Where the name comes from | How 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 predictions | Claude, GPT, Gemini. Different subagents can run different models |
| Token | The unit of text a model reads and writes — roughly ¾ of a word, or a word fragment | The 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 window | The total amount of text the model can hold at once — the instructions, your messages, the tool results, everything. Finite | Window from signal processing: a bounded view sliding over a longer stream. You see a portion, not the whole | The hard constraint behind almost every BMAD design choice. Run out and the model starts forgetting the beginning |
| Prompt | The text you send in | Theatre — the prompter feeds an actor their line from offstage | Draft mode's entire output is a prompt |
| Inference | One act of the model producing output | Logic/statistics: deriving a conclusion from inputs. Contrasted with training | "Inference cost" = what you pay per call |
| Training data / knowledge cutoff | What the model learned from, and the date it stops | Self-explanatory | The reason "no conclusions from training data" exists as a rule |
| Hallucination | The model stating something false with full confidence | Borrowed 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 word | The failure mode the citation discipline exists to prevent |
| Grounding | Tying output to a verifiable external source | Electrical grounding — connecting a circuit to earth so it has a stable reference | The entire point of Deep Recon |
2.2 Agents and tools
| Term | What it is | Where the name comes from | How it shows up here |
|---|---|---|---|
| Agent | A model that can take actions in a loop — call tools, observe results, decide what to do next — rather than just answering once | Latin 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 calling | Giving the model a set of callable functions (search the web, read a file, run a command) | Literal | Search, file writes, and MCP servers are all tools |
| Harness | The program that runs the loop around the model, handles tool calls, and manages files | From draft animals and, later, test harnesses in software: the rigging that connects a component to the thing driving it | Claude Code, Cursor, etc. BMAD skills run inside a harness |
| Subagent | A separate model instance spawned with its own fresh context and a narrow assignment, which reports back | Sub- = below. It's an agent working for an agent | The parallel researchers. Also called assistants in the docs |
| MCP (Model Context Protocol) | An open standard for connecting models to external data sources and tools | Literal description | How 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
| Word | AI meaning | BMAD meaning |
|---|---|---|
| Agent | A model acting in a loop with tools | A 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
| Term | What it is | Where the name comes from | How you use it |
|---|---|---|---|
| Skill | A directory with SKILL.md plus supporting files. The unit of installation and invocation | A packaged competence the agent acquires — the way a person learns a skill and can then perform it on demand | You invoke it: /bmad-deep-recon |
SKILL.md | The entry file. A router: reads your request, decides which branch, loads the right procedure | Convention | You never edit it directly; you override via TOML |
references/ | Procedure files loaded only if that branch is taken | Literal | Why the skill shrank from ~4,000 to ~2,100 tokens: detail moved here |
| Just-in-time loading | Loading instructions only when needed | Borrowed from just-in-time manufacturing (Toyota, 1970s): don't stock parts until the moment of use, because inventory is waste | The core token-optimization pattern. Context is the inventory |
| Slash command | Typing /name to invoke something | IRC and chat clients, 1990s: /join, /me | How you start every skill |
| Workflow | A multi-step skill with a defined sequence | Business process management | Deep Recon is a workflow |
| Module | A bundle of skills shipped together. core = everything; bmm = software development | Software modularity | Deep Recon moved from bmm to core in PR #2611, so non-software installs get it |
| Shim | A stub that forwards an old name to a new one | Mechanical: a thin wedge inserted to fill a gap or level a surface. Software borrowed it for thin compatibility layers | bmad-market-research still works; it forwards to Deep Recon with type=market |
| TOML | The config file format (customize.toml) | "Tom's Obvious Minimal Language," named for its creator, Tom Preston-Werner | Where you set validation level, source policies, output format |
| Frontmatter | A metadata block at the top of a markdown file | Book publishing: the front matter is everything before the main text — title page, copyright, contents. Static site generators (Jekyll, ~2008) borrowed it for the YAML block | Carries research.md's provenance so downstream skills can consume it |
| Artifact | A file produced by a process, kept as evidence of it | Archaeology: an object made by a human, evidence of activity. Software took it for build outputs | research.md is the artifact; the memlog is its excavation record |
| Planning artifacts | The output folder where analysis and planning documents land | Compound of the above | Where your run folders go |
| Memlog | An append-only file recording what happened, in order | Memory + log. Append-only is the load-bearing part: you add, never edit | Survives 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:
- 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.
- 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.
- 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:
| Sense | Example | Meaning |
|---|---|---|
| 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.
refreshre-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:
| Mode | Where the crawling happens | You supply | Pick it when |
|---|---|---|---|
| Draft | Elsewhere — ChatGPT, Gemini, Perplexity | One paste, one round-trip | You already pay for a deep-research subscription |
| Process | Already happened, somewhere | A finished report | Someone handed you a document |
| Run | Here, in this session | Approval at one gate | You 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:
- 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.
- 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.
- 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:
| Action | When | Why it matters |
|---|---|---|
| Delete a dimension | It doesn't bear on the decision | Each costs a full fan-out. At deep, cutting one saves ~15% of the run |
| Add a dimension | Something decision-critical is missing | The pack is generic; only you know your constraints |
| Split a dimension | It's actually two questions with different sources | Merged dimensions produce shallow answers on both |
| Merge two | They'd hit the same sources | Removes 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:
| Topology | Shape | Use when |
|---|---|---|
| Breadth-first | One plan → N assistants → N independent sub-questions, in parallel | The dimensions are genuinely independent (the usual case) |
| Depth-first | One question, several assistants attacking it from different angles, iterating | One contested question where the disagreement is the problem |
| Straightforward | One assistant, small budget | A 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:
- 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.
- 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
| Term | What it is | Where the name comes from |
|---|---|---|
| Round | One full cycle of parallel searching, after which results are assessed and the next cycle assigned | Boxing, tournaments, negotiations: a bounded phase followed by reassessment |
| Lead | A finding that points toward something worth chasing next — a contradiction, an unexpected connection | Journalism: a lead is a tip pointing at a story. Round 2's assignments come from round 1's leads |
| Tool-call budget | A cap on how many searches one assistant may make | Old French bougette, "little pouch" — a bounded allocation. Prevents one assistant eating the whole run |
| Effort preset | A named bundle of settings: quick / standard / deep | Preset from audio and camera equipment: stored settings recalled by name |
| Stop-and-write valve | The rule that says commit what you have rather than spiralling | Valve: 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
| Term | What it is | Where the name comes from |
|---|---|---|
| Brief | The narrow assignment handed to one subagent — written to a file, never a command line | Legal brief (a condensed statement of a case) and military briefing. Both mean: the minimum someone needs to act, deliberately short |
| Digest | Extracted, arranged claims from a source | Latin 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 |
| Claim | A single assertion, carrying a source, dates, and a status | Legal and philosophical: an assertion put forward as true, awaiting support |
| Status | unverified → verified → disputed → overturned | Latin status, "state." The point is that a claim's condition is tracked and visible, not silently promoted to fact |
| Import | A finished external report, preserved untouched | Trade: 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
| Term | What it is | Where the name comes from |
|---|---|---|
| Staleness | A claim that was true when gathered and may not be now | Literal — 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 window | How long a class of claim stays trustworthy. Statute ~36 months; enforcement guidance ~6 | Same caching lineage — essentially a TTL per claim class |
| Staleness map | The section of the report listing what ages fastest and when to re-check | Cartographic metaphor: a map of where the ground is likely to move |
| Refresh | Re-verify only the stale claims; produce a delta report | Caching again: refreshing a cache entry re-fetches it |
| Delta | The difference: confirmed / changed / overturned | Greek letter Δ, standard notation for change |
| Deepen | Drill further into one dimension without re-running the others | Literal |
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
| Term | What it is | Where the name comes from |
|---|---|---|
| Provenance | The documented origin and chain of custody of a claim | French 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 marker | The inline [n] pointing to a source | Latin citare, "to summon" — literally calling a witness |
| Source appendix | The numbered list the markers resolve to | Latin appendere, "to hang upon" — attached matter |
| Cross-check | The mechanical script verifying every marker has an entry and vice versa | Bookkeeping: reconciling two records against each other |
| Verification level | normal / high / max — how much gets checked | Latin verus, "true" — making true, or showing to be so |
| Red team | An adversarial pass that attacks the conclusion | Cold 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
| Term | What it is | Where the name comes from |
|---|---|---|
| Slug | The short deterministic identifier for a run folder | Newspaper 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 |
| Deterministic | Same input always produces the same output | Latin determinare, "to limit." Why it matters: the run slug must be identical across draft → process → refresh, or your artifacts scatter |
| Single-writer rule | Subagents return digests to the lead; only the lead writes research.md | Concurrency control. Parallel writes to one file race and corrupt. Fan out reads, serialize writes |
| Headless | Running without an interactive user or display | Headless server / headless browser: no "head" = no monitor. Here it means CI, so output defaults to plain markdown |
| Idempotent | Running it twice has the same effect as running it once | Latin idem ("same") + potens ("power"). Why refresh is safe to schedule |
| Determinism kit | recon_kit.py — the script owning counting, date math, and slug generation | Because 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:
| Metaphor | What it correctly suggests | Where it misleads |
|---|---|---|
| "Run" | Bounded, configured, identified, resumable | Implies 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 inquiry | Implies 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 construction | Implies 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 holds | Implies 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
quickengagement 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
| Resource | Why |
|---|---|
github.com/bmad-code-org/BMAD-METHOD | The repo. Read docs/explanation/ before docs/how-to/ — explanation gives you the model, how-to gives you the steps |
docs/explanation/deep-recon.md | The primary source for everything in Part 0 and the main guide |
| PR #2611 | The 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.md | Config layering and merge rules |
The skill's own customize.toml | The authoritative list of knobs, always ahead of any documentation |
8.2 Agentic systems generally
| Resource | Why |
|---|---|
| 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.io | The standard behind external_sources |
| Anthropic's Agent Skills documentation | The skill-as-loadable-competence pattern BMAD builds on |
| Simon Willison's blog, prompt-injection tag | The 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
| Topic | Where to learn it |
|---|---|
| BFS / DFS | Cormen, 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-out | Any digital logic text for the electronics origin; distributed-systems literature for the modern sense |
| Append-only logs | Kleppmann, Designing Data-Intensive Applications, ch. 3 — the best explanation of why immutable logs are the backbone of trustworthy systems |
| Single-writer / concurrency | Same book, ch. 7 |
| Cache TTL and staleness | Any HTTP caching primer; the concept transfers directly |
8.4 Epistemology — for §22 of the main guide
All free online:
| Resource | Why |
|---|---|
Stanford Encyclopedia of Philosophy (plato.stanford.edu) — entries on Epistemology, The Analysis of Knowledge, Epistemological Problems of Testimony, Scientific Objectivity | Rigorous, 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
| Resource | Why |
|---|---|
| PRISMA guidelines for systematic reviews | The 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 framework | How 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 crisis | Why "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 gistSynced 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
- At a Glance
- Where Research Sits in the Lifecycle
- Choosing the Right Research Type
- The Three Research Workflows in Detail
- The Sub-Agent Architecture
- Research Depth and Scale-Adaptive Intelligence
- End-to-End Walkthrough
- Scenario Library
- Multiple and Parallel Agents (Party Mode)
- Where the Facts Come From (Provenance and Retrieval)
- Cost, Context, and Running Off-IDE
- Brownfield Research and Established Projects
- Worked Example: A User Recommendation System
- Markov Decision Processes and Stochastic Dependency Graphs
- Observations and Findings
- Practical Tips and Best Practices
- Quick Reference
- Glossary
- Appendix A: Copy-Paste Research Prompts
- Appendix B: Custom Party Definitions
- 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"):
| Workflow | Command (short form) | Menu code | Answers |
|---|---|---|---|
| Market Research | bmad-market-research | MR | Who else is in this space? TAM/SAM/SOM, competitors, trends, sentiment |
| Domain Research | bmad-domain-research | DR | What must I know about this domain? Terminology, regulation, SME depth |
| Technical Research | bmad-technical-research | TR | Is 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 underbmad/bmm/workflows/1-analysis/research/. If one form isn't recognized, try the other or askbmad-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-artifactsproject_knowledge→docs(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-helpand 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-agent | Responsibility |
|---|---|
bmm-market-researcher | Market intelligence, TAM/SAM/SOM sizing |
bmm-data-analyst | Quantitative analysis, business metrics |
bmm-trend-spotter | Trend detection and disruption forecasting |
bmm-competitor-analyzer | Competitive positioning |
bmm-user-researcher | Personas 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-helpwhat 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:
- Built-in sub-agent fan-out — automatic, inside a single workflow. Market research already does this (§5). You don't control it directly.
- Party Mode — you 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-teammode (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:
| Mode | What it does | Reach for it when | Cost |
|---|---|---|---|
session | Default. One model voices every persona inline. | Banter, brainstorming, quick back-and-forth. | Cheapest |
auto | Voices inline for light rounds; spawns independent agents only when independence changes the answer. | Speed most of the time, independence on hard rounds. | Medium |
subagent | Spawns a separate agent per persona every substantive round. | Honest reviews and focus groups — voices must not bleed together. | High |
agent-team | Personas stand up as a persistent team addressing each other directly. Claude Code only. | A live, hands-off round-table. | Highest |
Fallback chain: agent-team → subagent → session 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
memlogprimitive (_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-teamspin up a separate agent per persona per round, so an N-persona panel is roughly N× the per-round cost ofsession. 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 withbmad-investigate(§4) when the question is "why is it behaving like this." - Feed
project-context.mdinto 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:
| Pass | Decision under research | Workflow |
|---|---|---|
| A — Scoring & representation | How do we model taste and score candidates? (EMA profiles, vectorization, hybrid) | bmad-technical-research |
| B — Failure modes | What goes wrong and how do we defend? (duplicates, cold start, popularity bias, staleness, feedback loops) | bmad-technical-research |
| C — Architecture & deployment | How does this run on AWS within latency/cost/DR constraints? | bmad-technical-research |
| D (light) — Domain | How 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−α)·uper interaction (or time-decayedweight = 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 mode | Mitigation | Pipeline 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_hits | Filter / candidate-gen |
| No diversity (five near-identical items) | MMR rerank: score = λ·rel − (1−λ)·max_sim_to_selected | Rerank |
| Cross-session repetition (already-seen items) | Impression discounting — decay recently-shown-not-clicked, don't hard-exclude | Rerank / post |
| Popularity bias (head drowns the tail) | Inverse-propensity weighting / popularity normalization | Scoring |
| Cold start (new user / new content) | Content-based ANN fallback + popularity/editorial prior | Candidate-gen |
| Staleness / filter bubble | Exploration — ε-greedy or bandit slot injecting off-profile candidates | Post |
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
γ^tof 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:
- 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.
- Model unknown (you only get to act and observe). This is reinforcement
learning. Q-learning updates value estimates from sampled transitions:
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.Q(s,a) ← Q(s,a) + α·[ r + γ·max_{a′} Q(s′,a′) − Q(s,a) ]
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:
| Element | Recommender meaning |
|---|---|
| S — state | User context: the EMA/user vector + session features + recently-shown impressions + time-of-day, etc. |
| A — action | Which item or slate to recommend now (slate MDPs are the multi-item case). |
| P(s′|s,a) — transition | The 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) — reward | Engagement signal: click, dwell time, or a longer-horizon retention/return signal. |
| γ — discount | How 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):
- Contextual bandit (γ = 0) over the rerank slot — the smallest useful step, and a principled version of the ε-greedy exploration you already flagged.
- Slate bandit / slate MDP — handle that you recommend a set, where items interact (diversity, position bias); SlateQ-style decomposition tames the combinatorial action space.
- 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-researchsubject, 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-helpis the source of truth. subagentgrounding 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.yamlsub-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
- 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.
- 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.
- One decision per pass. Fan out across passes, then convene a panel (§9.4).
- Match Party Mode mode to intent —
sessionfor ideation,subagentfor honest critique/focus groups,agent-teamfor hands-off debate. - Verify subagent grounding before trusting a panel on real code/data (§9.2).
- Route durable research to
docs/, cycle-specific toplanning-artifacts/. - Feed
project-context.mdinto research so findings respect your stack from the first token (§12). - Offload to Web Bundles when tokens matter — run the heavy research phase on a flat-rate GPT, bring artifacts back (§11).
- Take the keepsake HTML and attach it to the ticket; it's a real artifact.
- Remember the boundary — research designs the experiment; your data picks the winner (§13.7).
- Use
bmbto close the sub-agent gap if you do technical research often on the same stack. - 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)
| Agent | Persona | Codes |
|---|---|---|
| Analyst | Mary | BP, CB, MR, DR, TR |
| Product Manager | John | CP, VP, EP, CE, IR, CC |
| Architect | Winston | CA, IR |
| Scrum Master | Bob | SP, CS, ER, CC |
| Developer | Amelia | DS, CR |
| QA Engineer | Quinn | QA |
| UX Designer | Sally | CU |
| Technical Writer | Paige | WD, MG, VD, EC |
| Quick Flow Solo Dev | Barry | QS, QD, CR |
Output → _bmad-output/planning-artifacts/ (cycle) or docs/ (durable).
Feeds → bmad-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 file —
step-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-helpnext-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 (
subagentmode). - 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−α)·ufor 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 gistSynced 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_clipfield is configured withspace_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
- Background: the words and ideas you'll need
- The building block: what a vector is
- What a "norm" is, and where "L2" comes from
- Three ways to measure similarity (with a hand-worked example)
- The key insight: normalization makes all three agree
- Why
innerproductspecifically requires you to normalize - Why faiss and 16x compression do NOT mean "normalized"
- How to verify it yourself (the exact steps)
- The proof, using your five sampled vectors
- Conclusion
- How to fix it
- Glossary
- 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'sinnerproductis 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_type | What the engine computes | Divides by ‖A‖·‖B‖? | _score reported |
|---|---|---|---|
cosinesimil | cosine of the angle | Yes — built in | 1 + cos(A,B) (range 0–2) |
innerproduct | raw dot(A,B) | No | dot+1 if dot≥0, else 1/(1−dot) |
l2 | squared Euclidean distance | No (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
innerproductit reportsdot + 1when the dot is ≥ 0 and1/(1 − dot)when negative. This is monotonic — it never changes the ranking order — but it does mean the_scoreyou 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;
nbits can represent2ⁿ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:
| compression | bits/dim | levels (2ⁿ) | note |
|---|---|---|---|
| 32x | 1 | 2 | true binary quantization |
| 16x | 2 | 4 | your field |
| 8x | 4 | 16 | |
| 4x | 8 (byte) | 256 | |
| 2x | 16 (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_diskrandom rotation).on_diskapplies 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) | Dim | L2 norm | Largest |component| | RMS per component |
|---|---|---|---|---|
MT1IMGOST00034M2TL | 512 | 9.1736 | 3.3277 | 0.4054 |
MT1NURPHO000YDLE5G | 512 | 9.7454 | 5.3205 | 0.4307 |
MT1NURPHO0003GDRTT | 512 | 10.7579 | 6.1741 | 0.4754 |
RC2VZKAGA2RW | 512 | 10.4605 | 5.0318 | 0.4623 |
MT1SIPA000UKMLE2 | 512 | 9.6490 | 3.6192 | 0.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) product —
space_type: innerproductdoes no length-division. - Euclidean? No — the
space_typewould readl2. - Cosine in effect? No.
innerproductequals 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).
nbits encode2ⁿ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 itinnerproduct. - 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, orl2).- Unit sphere / unit vector — the set of all length-1 vectors / a vector of length 1.
12. References
- OpenSearch — Spaces (
space_typedefinitions and score formulas forcosinesimil,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_diskmode, two-phase rescoring,oversample_factordefault 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_L2for 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 gistSynced 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.
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
- 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").
- 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).
- Sort dimensions by the base vector's value (argsort). All plots use this shared column/x order.
- 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 gistSynced 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
- The Problem: Why GPS Alone Is Not Enough
- The Hardware Layer: Sensors Inside the iPhone
- 2.1 Accelerometer
- 2.2 Gyroscope
- 2.3 Barometer
- 2.4 GPS / GNSS Receiver
- 2.5 The Motion Coprocessor (M-series chip)
- The Framework Layer: Apple's CoreMotion & CoreLocation
- Signal Characteristics: What Each Activity Looks Like
- The Physics and Mathematics Behind Each Signal
- Full Implementation Walkthrough
- 6.1 Project setup and permissions
- 6.2 ActivityType and ActivityResult models
- 6.3 ActivityDetector class structure
- 6.4 Signal 1: CMMotionActivityManager
- 6.5 Signal 2: CMPedometer
- 6.6 Signal 3: Raw accelerometer variance
- 6.7 Signal 4: GPS speed via CLLocationManager
- 6.8 The fusion classifier — vote-based decision logic
- 6.9 Wiring it into a ViewController
- Scenario Walkthroughs
- 7.1 Scenario A: Normal sidewalk walk
- 7.2 Scenario B: Driving on a motorway
- 7.3 Scenario C: Sitting in a slow-moving traffic jam
- 7.4 Scenario D: Passenger in a car (not driving)
- 7.5 Scenario E: Running for a bus
- 7.6 Scenario F: GPS-denied environment (underground car park)
- 7.7 Scenario G: Cycling
- 7.8 Scenario H: Treadmill walking
- Edge Cases and Known Failure Modes
- Real-World Use Cases and Applications
- Battery and Performance Considerations
- Improving Accuracy: Advanced Techniques
- Privacy, Permissions, and App Store Compliance
- Testing Strategy
- Broader Perspectives: How Other Platforms Approach This
- 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:
| Rate | Typical use |
|---|---|
| 10 Hz | Background step detection, coarse activity |
| 50 Hz | Real-time activity classification (our use case) |
| 100 Hz | Gesture 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
CMDeviceMotionAPI 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 datecurrentPace— current pace in seconds per metre (⚠ not per step)currentCadence— steps per second (iOS 9+)distance— estimated walking distance in metresfloorsAscended/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
| Activity | Typical speed range | Notes |
|---|---|---|
| Stationary | 0–0.3 m/s | GPS noise floor; readings oscillate even when standing still |
| Slow walk | 0.5–1.2 m/s | Older adults, browsing on a phone |
| Normal walk | 1.2–1.8 m/s | Average adult pace (~4.5 km/h) |
| Brisk walk | 1.8–2.2 m/s | Commuter pace |
| Running (slow jog) | 2.2–4.0 m/s | 8–14 min/km |
| Running (fast) | 4.0–7.0 m/s | Sub-8 min/km |
| Cycling (slow) | 3.0–5.0 m/s | Urban cycling |
| Cycling (fast) | 5.0–12.0 m/s | Road cycling |
| Traffic jam driving | 0–2.0 m/s | Overlap with walking range |
| Urban driving | 6.0–14.0 m/s | 22–50 km/h |
| Motorway driving | 22–40+ m/s | 80–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
| Activity | Steps/second | Notes |
|---|---|---|
| Stationary | 0 | Occasional fidget may produce 0.1–0.3 briefly |
| Strolling | 0.8–1.2 | Window shopping, corridors |
| Normal walk | 1.4–1.8 | Most adults |
| Brisk walk | 1.8–2.5 | Exercise walking |
| Running | 2.5–4.0 | Increases with speed |
| Sprint | 4.0–5.0 | Elite sprinters up to 5.5 |
| Cycling | 0 | No footsteps (pedalling doesn't register) |
| Driving | 0 | No 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.
| Activity | Typical variance | Explanation |
|---|---|---|
| Stationary on table | <0.001 | Essentially no motion |
| Held stationary | 0.002–0.010 | Micro hand tremors |
| Driving (smooth motorway) | 0.005–0.020 | Engine vibration, smooth road |
| Driving (city with potholes) | 0.020–0.100 | Road surface irregularities |
| Walking (in pocket) | 0.020–0.080 | Hip-transmitted step bounce |
| Walking (in hand) | 0.050–0.200 | Arm swing adds to hip movement |
| Running | 0.100–0.400 | High-impact footstrikes |
| Cycling | 0.010–0.050 | Smooth 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:
| Signal | Walking | Driving | Running | Cycling | Stationary |
|---|---|---|---|---|---|
| GPS speed (m/s) | 0.5–2.2 | >6 (usually >10) | 2.2–7 | 3–12 | 0–0.3 |
| Cadence (steps/s) | 1.2–2.5 | ~0 | 2.5–5 | ~0 | ~0 |
| Accel variance | 0.02–0.2 | 0.005–0.05 | 0.1–0.4 | 0.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:
- Low-pass filtering to remove high-frequency noise (the coprocessor does this in hardware)
- Finding local maxima above a threshold (~1.2 g for the magnitude)
- 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 & Capabilities → Background 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:
| Scenario | GPS votes | Cadence votes | Var votes | Total | Confidence |
|---|---|---|---|---|---|
| Walking, GPS available | 2 (walk) | 2 (walk) | 1 (walk) | 5 walk | High |
| Driving at 60 km/h | 3 (drive) | 1 (drive) | 1 (drive) | 5 drive | High |
| Traffic jam (5 km/h) | 2 (walk) | 1 (drive) | 1 (drive) | 2 walk, 2 drive | Low — tie |
| No GPS, walking | — | 2 (walk) | 1 (walk) | 3 walk | Medium |
| Running | 2 (run) | 2 (run) | 1 (walk) | 4 run, 1 walk | Medium-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.
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 1.5 m/s (valid, accuracy ±0.3 m/s) | +2 walking |
| Cadence | 1.7 steps/sec | +2 walking |
| Accel variance | 0.045 | +1 walking |
| Total | 5 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.
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 30.5 m/s (valid) | +3 driving |
| Cadence | 0.0 steps/sec | +1 driving |
| Accel variance | 0.008 (smooth tarmac) | +1 driving |
| Total | 5 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.
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 0.8 m/s | +2 walking (below walkingSpeedMax of 2.2) |
| Cadence | 0.0 steps/sec | +1 driving |
| Accel variance | 0.015 (small bumps) | borderline — 0 votes |
| Total | 2 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:
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 4.5 m/s | +2 running |
| Cadence | 3.2 steps/sec | +2 running |
| Accel variance | 0.22 | +1 walking |
| Total | 4 running, 1 walking — Medium confidence running |
At the bus stop (after 5 seconds standing):
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 0.1 m/s (GPS noise at standstill) | +2 walking |
| Cadence | 0.0 | +1 driving |
| Accel variance | 0.003 (very still) | +1 driving |
| Total | 2 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.
| Signal | Value | Vote |
|---|---|---|
| GPS speed | -1 (invalid) | No votes cast |
| Cadence | 1.6 steps/sec | +2 walking |
| Accel variance | 0.040 | +1 walking |
| Total | 3 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.
| Signal | Value | Vote |
|---|---|---|
| GPS speed | 6.0 m/s | Exactly at drivingSpeedThreshold — votes +3 driving |
| Cadence | 0.0 steps/sec (no footsteps on pedals) | +1 driving |
| Accel variance | 0.018 (smooth bike path) | borderline — 0 votes |
| Total | 4 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).
| Signal | Value | Vote |
|---|---|---|
| GPS speed | ~0.1 m/s (GPS noise) | +2 walking |
| Cadence | 1.6 steps/sec | +2 walking |
| Accel variance | 0.035 | +1 walking |
| Total | 5 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
Navigation apps (Apple Maps, Google Maps, Waze)
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
| Component | Approximate current drain | Notes |
|---|---|---|
| CMMotionActivityManager | ~0.1 mA | Runs on coprocessor; negligible |
| CMPedometer | ~0.1 mA | Coprocessor-assisted |
| CMMotionManager (50 Hz) | ~0.5–1 mA | Main CPU required; stops in background |
| CLLocationManager (Best accuracy) | ~20–30 mA | Highest drain; use only when needed |
| CLLocationManager (100m accuracy) | ~3–5 mA | Acceptable for coarse activity |
| Total (full stack) | ~22–32 mA | GPS 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:
- Open Create ML app in Xcode
- Create new Activity Classifier project
- Set prediction window size: 50 samples (1 second at 50 Hz)
- Drag in your labelled data folders
- Train — Create ML automatically extracts features (mean, variance, FFT peaks, etc.)
- Export the
.mlmodelfile
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
CMMotionActivityManagerorCMPedometer - 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:
- Walking test: Walk around a building exterior for 2 minutes; verify consistent walking detection with high confidence.
- Driving test: Drive on a suburban road for 5 minutes including stops at traffic lights; verify driving detection survives the stops.
- Transition test: Walk to a car, get in, start driving; measure transition detection latency.
- GPS-denied test: Walk through a shopping centre or car park; verify fallback to motion-only classification.
- 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/cmmotionactivitymanagerCMPedometer— https://developer.apple.com/documentation/coremotion/cmpedometerCMMotionManager— https://developer.apple.com/documentation/coremotion/cmmotionmanagerCLLocationManager— https://developer.apple.com/documentation/corelocation/cllocationmanagerCLLocation.speedandspeedAccuracy— 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
Related iOS APIs
HKWorkoutSession— HealthKit workout tracking integrated with activity detectionCMFallDetectionManager— 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 gistSynced 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:
- Product summary
- Primary workflows
- Source and discovery model
- Skill validation model
- Storage architecture
- Update/revision tracking model
- CLI command design
- TUI design
- Autocomplete design
- Install/update/remove lifecycle
- CI and lockfile behavior
- Data model at a conceptual level
- Failure modes and how the tool should report them
- MVP scope
- Future enhancements
- 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 gistSynced verbatim from gist.github.com/bl9.
Apache Tez: A Deep Dive Into Architecture, Internals & Source Code
Table of Contents
- What Is Apache Tez?
- Why Tez Exists — The MapReduce Problem
- Architecture Overview
- Core Concepts
- Source Code Structure
- Deep Dive: Key Modules
- The DAG Execution Lifecycle
- The Shuffle Pipeline
- Runtime Reconfiguration
- How Hive and Pig Use Tez
- Tez on YARN — The Full Picture
- Reading the Source: A Guided Path
- Simpler Open Source Alternatives
- 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
| Metric | Value |
|---|---|
| Language | Java |
| Lines of Code | ~225,000 |
| Contributors | 109+ |
| First Commit | April 2013 |
| License | Apache 2.0 |
| Repository | github.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:
-
The data-processing pipeline engine — where you plug in Input, Processor, and Output implementations to perform arbitrary data processing within each task.
-
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
| Module | Approx. LOC | Complexity | What It Does |
|---|---|---|---|
tez-api | ~20k | Medium | User-facing API: DAG, Vertex, Edge builders, configuration |
tez-dag | ~70k | Very High | ApplicationMaster: DAG/Vertex/Task state machines, scheduling, container mgmt |
tez-runtime-internals | ~15k | High | Task-level runtime: manages the I/P/O pipeline lifecycle inside containers |
tez-runtime-library | ~30k | Very High | Shuffle, sort, merge, partitioning — the data movement engine |
tez-mapreduce | ~15k | Medium | Wraps MR Mapper/Reducer to run as Tez Processors |
tez-common | ~5k | Low | Utilities, counters |
tez-examples | ~2k | Low | Best starting point for reading the code |
tez-plugins | ~10k | Medium | History event logging, YARN task communicator |
tez-ui | ~20k | Medium | Web 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:
| Type | Description | Use Case |
|---|---|---|
ONE_TO_ONE | Task i in source → Task i in dest | Pipeline stages with same parallelism |
SCATTER_GATHER | All source tasks → all dest tasks (with partitioning) | Shuffle/sort (like MR's shuffle) |
BROADCAST | Every source task sends to every dest task | Broadcast joins (small table to all reducers) |
CUSTOM | User-defined routing via EdgeManagerPlugin | Specialized 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:
| Event | From State | To State | What Happens |
|---|---|---|---|
| DAG_INIT | NEW | INITED | Parse DAG plan, create VertexImpl objects |
| DAG_START | INITED | RUNNING | Start root vertices (those with no inputs from other vertices) |
| VERTEX_COMPLETED | RUNNING | RUNNING | Check if downstream vertices can start |
| ALL_VERTICES_DONE | RUNNING | COMMITTING | All vertices succeeded, commit outputs |
| DAG_COMPLETED | COMMITTING | SUCCEEDED | Final commit done |
| INTERNAL_ERROR | any | ERROR | Unrecoverable 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:
- Initializes its tasks based on input splits or configured parallelism
- Waits for the VertexManager to signal readiness
- Schedules tasks on available containers
- Tracks task completion, handles failures and retries
- 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 YARNContainerLauncherManager— launches tasks in containersTaskCommunicatorManager— 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:
- Initializes the configured Inputs
- Initializes the Processor
- Initializes the configured Outputs
- Calls
processor.run(inputs, outputs)— this is where user code executes - 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
| Class | Purpose |
|---|---|
OrderedPartitionedKVOutput | The "map output" — partitions, sorts, and optionally combines data for a scatter-gather edge |
OrderedGroupedKVInput | The "reduce input" — fetches shuffled data, merges sorted streams |
UnorderedKVOutput | Unpartitioned output (for broadcast edges) |
UnorderedKVInput | Reads broadcast data |
ShuffleManager | Coordinates fetching shuffle data from upstream tasks |
MergeManager | Manages the merge of sorted runs (in-memory and on-disk) |
IFile / IFileOutputStream | The intermediate file format for shuffle data |
ExternalSorter | Sorts 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:
- No HDFS materialization — intermediate data goes to local disk or stays in memory
- 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:
- Serializes the DAG plan as a Protocol Buffer message
- Uploads the plan + JARs + configuration to HDFS
- Submits a YARN application (or reuses an existing Tez session)
Step 3: ApplicationMaster Initialization
The Tez AM starts on a cluster node:
DAGAppMaster.main()— entry point- Registers with YARN ResourceManager
- Deserializes the DAG plan
- Creates
DAGImpl, which createsVertexImplobjects for each vertex
Step 4: Vertex Scheduling
The DAG starts root vertices (those with no upstream dependencies):
VertexImpldetermines parallelism (from input splits or configured value)VertexImplcreatesTaskImplobjects- Each task requests a YARN container (with data locality preferences)
- The
TaskSchedulerManagernegotiates containers from the ResourceManager
Step 5: Task Execution
For each scheduled task:
- The AM launches the task in an allocated container
TezTaskRunnerstarts in the container's JVM- The runtime initializes Input → Processor → Output
Processor.run()executes (this is where the actual computation happens)- The task reports status back to the AM via heartbeats
Step 6: Inter-Vertex Data Flow
When "Map 1" tasks produce output:
OrderedPartitionedKVOutputwrites sorted, partitioned data to local disk- The AM notifies "Reducer 2" that data is available
- "Reducer 2" tasks fetch their partitions from "Map 1" tasks via HTTP
ShuffleManager+MergeManagerhandle the fetch and merge
Step 7: Completion
- All tasks in the final vertex complete
- Output is committed (to HDFS or wherever configured)
- The DAG transitions to SUCCEEDED
- The AM reports completion to the client
- 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:
| Property | Default | Description |
|---|---|---|
tez.runtime.io.sort.mb | 100 MB | Size of the in-memory sort buffer |
tez.runtime.sort.spill.percent | 0.8 | Buffer threshold that triggers a spill |
tez.runtime.combine.min.spills | 3 | Minimum 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:
- Collects output-size statistics from completed upstream tasks
- Estimates the total output size
- Calculates the optimal parallelism for the downstream vertex
- 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.javaintez-runtime-library— the auto-parallelism logicVertexImpl.java— handles the reconfigure event, re-creates tasksVertexManager.javaintez-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:
- Hive compiles the HiveQL into an operator tree (Scan → Filter → Join → Aggregate → File)
- Hive's optimizer (Calcite-based) optimizes the plan
TezCompilertranslates 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
TezSessionStatesubmits the DAG to a running Tez session (or starts a new one)- Tez executes the DAG on YARN
- 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:
- Task A finishes in container C on node N
- The AM checks if any pending task prefers node N (data locality)
- If yes: reassign container C to that task — no YARN negotiation needed
- 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.
-
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. -
tez-examples/OrderedWordCount.java— Adds a second vertex for sorting. Shows a multi-vertex DAG with a SCATTER_GATHER edge. -
tez-api/DAG.java— Read the builder methods. Clean API. -
tez-api/Vertex.java— How vertices are configured. -
tez-api/Edge.java+EdgeProperty.java— How edges are defined. Pay attention toDataMovementType.
Level 2: The Task Runtime (2–4 hours)
Understand what happens inside each task.
-
tez-runtime-internals/LogicalIOProcessorRuntimeTask.java— The task entry point. Follows the I/P/O lifecycle clearly. -
tez-api/Processor.javainterface — Simple: justrun(Map<String, LogicalInput>, Map<String, LogicalOutput>). -
tez-runtime-library/OrderedPartitionedKVOutput.java— The producer side of shuffle. Follow fromwrite()through sort and spill. -
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.
-
Generate the state diagram first:
mvn compile -Pvisualize \ -Dtez.dag.state.classes=org.apache.tez.dag.app.dag.impl.DAGImpl \ -DskipTests=trueRender the
.gvfile and keep it open as a reference. -
tez-dag/DAGImpl.java— Focus on theStateMachineFactoryat 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. -
tez-dag/VertexImpl.java— The most complex file. Focus on:- The state machine definition
handleInitEvent()— how a vertex initializesscheduleTasks()— how tasks are scheduledreconfigureVertex()— runtime parallelism changes
-
tez-dag/TaskImpl.java— Simpler. Focus on attempt management. -
tez-dag/TaskAttemptImpl.java— Focus on launch, completion, and failure handling.
Level 4: Container & Scheduling (Advanced)
-
tez-dag/TaskSchedulerManager.java— How the AM interacts with YARN for containers. -
tez-dag/AMContainerMap.java— Container reuse logic. -
tez-runtime-library/ShuffleVertexManager.java— Auto-parallelism. TheonVertexManagerEventReceived()andreconfigureVertex()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 dependenciesworker.py— the worker that pulls and executes taskstask.py— the Task base class withrequires()andrun()
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:
- Build a DAG of function calls
- Identify tasks with no dependencies
- Execute them
- Remove completed tasks from the graph
- 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:
| Concept | Tez | Spark |
|---|---|---|
| Execution unit | DAG | Job |
| Stage | Vertex | Stage |
| Task | Task | Task |
| Shuffle | OrderedPartitionedKVOutput | ShuffleMapTask |
| Dynamic parallelism | ShuffleVertexManager | Adaptive Query Execution |
| Container reuse | AM-managed | Executor 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
SortShuffleWriterandExternalSorter— same concepts as Tez'sOrderedPartitionedKVOutput, but in Scala with better comments - Hadoop MapReduce's
MapOutputBuffer— the original implementation that Tez's shuffle is based on (inhadoop-mapreduce-client-core)
Summary Comparison Table
| Project | Language | Lines of Code | What It Teaches | Learning Time |
|---|---|---|---|---|
Dask local.py | Python | ~400 | Minimal DAG executor algorithm | 1 hour |
| Luigi | Python | ~15k | Dependency resolution, scheduling, retry | 1 day |
| Prefect core | Python | ~50k | Task/flow model, modern state management | 1–2 days |
| Tez examples | Java | ~2k | Tez API surface, I/P/O model | 2 hours |
| Ray core | Python/C++ | Large | Distributed object-based task execution | 2–3 days |
| Spark DAGScheduler | Scala | ~5k | Stage-based DAG execution (Tez competitor) | 1 day |
Tez tez-dag | Java | ~70k | Production DAG AM with full state machines | 1 week+ |
Tez tez-runtime-library | Java | ~30k | Production shuffle/sort/merge pipeline | 1 week+ |
Recommended Learning Path
- Read Dask
local.py— understand the core algorithm (1 hour) - Read Luigi's
scheduler.py— add dependency resolution and retry (half day) - Read Tez
tez-examples/WordCount.java— see how Tez exposes the DAG API (1 hour) - Read Tez
tez-api/DAG.java+Vertex.java+Edge.java— the user-facing API (2 hours) - Generate Tez's state diagram — visualize the AM's state machine (30 min)
- Read Tez
DAGImpl.javawith the state diagram open — the AM core (half day) - 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 gistSynced 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
;reviewor;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;
}