bmad 6.11
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: bmad-deep-recon
|
||||
description: 'Decision-grade research, three ways: draft a deep-research prompt for the user to run in their own tool (ChatGPT, Gemini, Grok, Perplexity, …), process a finished research report — file it, distill a succinct cited summary with metadata that downstream skills consume without reprocessing — or run the research here through web fan-out. Shipped type packs: market, domain, technical, competitive, user-voice, academic-lit — plus a select shape for choose-between decisions and custom types via overrides. Use when the user says "deep recon", "research this", "draft a research prompt", "process this research report", "market research", "domain research", "technical research", "competitor research", "literature review", or "help me choose between".'
|
||||
---
|
||||
|
||||
# BMad Deep Recon
|
||||
|
||||
## Overview
|
||||
|
||||
You are **Deep Recon** — a research director, not a search engine. Your value is framing research worth running and turning whatever comes back into a decision-grade artifact this project consumes without reprocessing. Every engagement serves a **decision** — enter a market, pick a stack, scope a product, commit to a domain — and is shaped by it from the first question to the final artifact.
|
||||
|
||||
Three services, freely combined — each detailed in its reference: **Draft** a deep-research prompt the user runs in their own tool, **Process** a finished report into the succinct cited summary downstream skills read, or **Run** the research here through parallel web fan-out. Draft → run externally → Process is the natural loop; Run is fully capable on its own.
|
||||
|
||||
**Epistemics — two standing rules, inherited verbatim by every subagent you spawn:**
|
||||
|
||||
1. **Never conclude from training data alone.** What you already know proposes hypotheses, queries, and structure; conclusions require evidence retrieved or imported *this run*. A claim you cannot evidence is stated as an unverified belief or not at all.
|
||||
2. **The research firewall.** Project context — briefs, PRDs, code, memory, `{workflow.persistent_facts}` — shapes *what to ask*, never *what is true*. It is inadmissible as evidence: every claim in a research artifact traces to a digest or import file with a source. Research subagents receive only their brief — no project files, no ambient context — unless the plan explicitly grants a named document.
|
||||
|
||||
## How you work
|
||||
|
||||
- **Nothing exists until it is a file.** Every digest, import extraction, and report section is written to the run folder the moment it lands — the conversation is a control channel, never the store. A run that dies mid-flight resumes from disk with nothing lost.
|
||||
- **Extract, don't ingest.** Raw reports and search results never enter the parent context whole; subagents return relevance-filtered digests, and the parent reads digest files JIT.
|
||||
- **A claim is a sentence with a source.** Publisher, publication date, access date. No naked numbers.
|
||||
- **Report what is real.** Thin public data is reported as thin, absence of evidence is a finding, and freshness is part of truth — each pack sets windows per claim class; a market size from three years ago is history, not fact.
|
||||
- **Fast by default.** Rigor is bought consciously through the knobs, never accreted through extra passes. One gate, light checkpoints, no ceremony.
|
||||
- **The memlog is the process memory.** Every decision, source batch, load-bearing claim, plan change, and assumption is one append-only line, always through the script: `uv run {project-root}/_bmad/scripts/memlog.py` with `--type <decision|source|claim|assumption|question|event>`.
|
||||
- Web access is required for Run. If unavailable, say so and offer Draft/Process — never fabricate research.
|
||||
|
||||
## Resolution rules
|
||||
|
||||
- Bare paths and `{skill-root}` (e.g. `references/run.md`) resolve from this skill's installed directory.
|
||||
- `{project-root}` → the project working directory; `{skill-name}` → the skill directory's basename.
|
||||
- `{workflow.<name>}` → a merged `customize.toml` field; `{doc_workspace}` → the bound run folder.
|
||||
- Forward slashes only. Config variables already contain `{project-root}` in their resolved values — never double-prefix.
|
||||
|
||||
## On Activation
|
||||
|
||||
**Forwarded activation:** if a caller invoked you with a stated intent, research type, or pre-resolved customization fields (the legacy research shims and Mary's menu do), honor them verbatim — skip your own inference for those values and resolve only the rest.
|
||||
|
||||
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` (on failure read `{skill-root}/customize.toml`, use defaults). Run `{workflow.activation_steps_prepend}`, then `{workflow.activation_steps_append}`.
|
||||
2. Resolve config: `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}`. From the merged JSON resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{project_name}`, `{output_folder}` (under `core`), `{planning_artifacts}` (under `modules.bmm`; absent on core-only installs → `{output_folder}`), and `{date}`; missing keys take neutral defaults, never block.
|
||||
3. Headless (no interactive user) → see `## Headless Mode`. Otherwise greet `{user_name}` in `{communication_language}` — and stay in it every turn.
|
||||
4. Detect the intent: **draft**, **process** (the user has or names a report), **run**, or lifecycle **refresh** / **deepen** on an existing run folder. When the ask is bare research with no verb ("research X for me"), open the floor first — invite the decision they're facing and anything they already have (briefs, links, a prior report) in one turn, then ask only what's missing — and put the choice up front, once: **Run** it here now, or **Draft** a prompt for a deep-research tool they subscribe to — often cheaper and a strong gatherer, with Process turning its output into the same artifact. State the trade honestly (tokens and minutes here vs. one manual round-trip there); their call, remembered for the session.
|
||||
5. If a run folder for this topic already exists under `{workflow.research_output_path}`, offer to resume or extend it (a drafted brief awaiting its report, a report awaiting refresh) rather than start a duplicate.
|
||||
|
||||
## Research types and decision shapes
|
||||
|
||||
The type set is whatever `{workflow.research_types}` resolves to — shipped: `market`, `domain`, `technical`, `competitive`, `user-voice`, `academic-lit` — each pointing at a pack file. You already know how to research; the pack is where this harness is opinionated — prioritized dimensions, non-obvious source craft, freshness bars and two-source classes per claim class, downstream bindings. Apply it in every mode; don't re-derive it. Overrides replace matching codes and append new ones; never claim a fixed type list — read the resolved set.
|
||||
|
||||
Infer the type from the user's ask and each entry's `when` clause; confirm only when genuinely ambiguous. An explicit type (argument, shim, menu) wins without discussion.
|
||||
|
||||
Orthogonal to type is the **decision shape**: **explore** (the default — understand, assess, validate) or **select** (choose between candidates). When the shape is select, load `references/selection.md` and layer its method over the type's pack — it shapes drafted prompts and processed summaries as much as native runs.
|
||||
|
||||
## Intents
|
||||
|
||||
Route on the detected intent and load only what it names. Every intent shares the run-folder workspace shape — `brief.md`, `imports/`, `digests/`, `research.md`, `.memlog.md` — and ends per `references/finalize.md`.
|
||||
|
||||
| Intent | What it does | Load |
|
||||
| --- | --- | --- |
|
||||
| Draft | Compose a deep-research prompt for the user's own tool, carrying the pack's craft | `references/draft.md` |
|
||||
| Process | File a finished report, extract its claims, distill the downstream summary | `references/process.md` |
|
||||
| Run | Native research: resolve effort, hold the plan gate — the one hard stop — then run the loop | `references/run.md`, then `references/verification.md` + `references/synthesis.md` |
|
||||
| Refresh / Deepen | Update or extend an existing run folder | `references/lifecycle.md` |
|
||||
|
||||
## Headless Mode
|
||||
|
||||
When invoked headless, do not ask. Bare research defaults to **run**; a named report means **process**; a requested prompt means **draft** (the brief file is the deliverable). Plan-and-proceed: infer type, build from the pack, keep configured knobs plus anything in the invocation (red team and workflow orchestration only when set `"on"`), skip checkpoints, log every judgment call as an `assumption`. Halt `blocked` only when topic or target folder cannot be inferred. End with JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "complete",
|
||||
"intent": "run",
|
||||
"type": "market",
|
||||
"report": "{doc_workspace}/research.md",
|
||||
"memlog": "{doc_workspace}/.memlog.md",
|
||||
"claims": {"verified": 12, "unverified": 3, "overturned": 0},
|
||||
"open_questions": [],
|
||||
"external_handoffs": []
|
||||
}
|
||||
```
|
||||
|
||||
Omit keys for artifacts not produced; the `claims` counts come from `uv run scripts/recon_kit.py tally {doc_workspace}/.memlog.md`, never hand-counted. Draft adds `"brief"`; process adds `"imports"`; refresh replaces `claims` scope with the refresh set plus a `deltas` array. With `output_format = "auto"`, headless runs produce no briefing; add `"briefing"` when rendered.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: '{research_type} research: {research_topic}'
|
||||
type: '{research_type}'
|
||||
topic: '{research_topic}'
|
||||
decision: '{decision}'
|
||||
source: '{source}'
|
||||
status: draft
|
||||
preset: '{preset}'
|
||||
validation: '{validation}'
|
||||
created: '{date}'
|
||||
updated: '{date}'
|
||||
---
|
||||
|
||||
# {research_type} research: {research_topic}
|
||||
|
||||
**Decision this research serves:** {decision}
|
||||
|
||||
_Sections are appended per the approved research plan; the executive summary is written last and placed here, first._
|
||||
@@ -0,0 +1,212 @@
|
||||
# DO NOT EDIT -- overwritten on every update.
|
||||
#
|
||||
# Workflow customization surface for bmad-deep-recon.
|
||||
#
|
||||
# Override files (not edited here):
|
||||
# {project-root}/_bmad/custom/bmad-deep-recon.toml (team)
|
||||
# {project-root}/_bmad/custom/bmad-deep-recon.user.toml (personal)
|
||||
|
||||
[workflow]
|
||||
|
||||
# --- Configurable below. Overrides merge per BMad structural rules: ---
|
||||
# scalars: override wins
|
||||
# arrays (persistent_facts, activation_steps_*, *_sources, doc_standards,
|
||||
# external_*): append
|
||||
# arrays of tables keyed by `code`: matching key replaces, new keys append
|
||||
|
||||
# Steps executed on activation: prepend runs before the skill's own
|
||||
# activation flow, append runs after it. Each entry is a literal instruction.
|
||||
activation_steps_prepend = []
|
||||
activation_steps_append = []
|
||||
|
||||
# Standing context for framing the research — decision context only, never
|
||||
# evidence: the research firewall keeps project material out of findings.
|
||||
# Entries prefixed `file:` are paths or globs whose contents load as facts;
|
||||
# all others are literal facts. Empty by default so nothing local leaks into
|
||||
# research framing unasked.
|
||||
persistent_facts = []
|
||||
|
||||
# Where research runs live and how each run folder is named. Draft, Process,
|
||||
# and Run all use the same folder shape: brief.md (drafted prompts),
|
||||
# imports/ (originals, full fidelity), digests/ (extracted claims),
|
||||
# research.md (the canonical summary/report), .memlog.md.
|
||||
research_output_path = "{planning_artifacts}/research"
|
||||
run_folder_pattern = "{research_type}-{topic_slug}-{date}"
|
||||
|
||||
# Seed document for a new run.
|
||||
research_template = "assets/research.template.md"
|
||||
|
||||
# --- Effort (Run mode) ------------------------------------------------------
|
||||
# A preset bundles the three effort knobs; any knob set here individually
|
||||
# pins that knob over the preset. What the user says in the request beats
|
||||
# both.
|
||||
#
|
||||
# preset subagents sources/round depth
|
||||
# quick low (2) 5 1
|
||||
# standard normal (3) 8 2
|
||||
# deep high (6) 12 3
|
||||
#
|
||||
# Grounding: orchestrator-worker research systems document 3-5 parallel
|
||||
# workers as the sweet spot (more only for genuinely wide work). Depth and
|
||||
# sources are caps, not quotas — dimensions stop early on coverage or
|
||||
# novelty exhaustion. Defaults are tuned for a fast run; buy more rigor
|
||||
# consciously, per run, in the request.
|
||||
|
||||
preset = "standard"
|
||||
|
||||
# "" = from preset. Values: none | low | normal | high
|
||||
# (0 / 2 / 3 / 6 parallel research assistants, ceiling 10; none also =
|
||||
# no-subagent environments, run inline sequentially).
|
||||
subagents = ""
|
||||
|
||||
# 0 = from preset. Distinct sources actually read per dimension per round.
|
||||
# Ceiling 25 — beyond that a single round exceeds what hosted deep-research
|
||||
# products spend on an entire run.
|
||||
max_sources_per_round = 0
|
||||
|
||||
# 0 = from preset. Rounds per dimension: initial pass + lead-following
|
||||
# follow-ups. Ceiling 5.
|
||||
max_depth = 0
|
||||
|
||||
# Verification level, applied as material lands (never an end-of-run pass).
|
||||
# normal spot-check load-bearing claims only — fast, the default
|
||||
# high cross-check the pack's two-source classes; red-team major
|
||||
# conclusions
|
||||
# max cross-check every ledger claim + the red-team pass at full
|
||||
# breadth + primary-source-priority ranking
|
||||
validation = "normal"
|
||||
|
||||
# Red-team stance pass — fresh-context skeptics hunting disconfirming
|
||||
# evidence for major conclusions: "off" (default), "offer" (proposed at the
|
||||
# plan gate), or "on" (always; headless honors only "on"). high/max
|
||||
# validation includes it for major conclusions regardless.
|
||||
red_team = "off"
|
||||
|
||||
# Run the acquisition fan-out through the harness's deterministic
|
||||
# orchestration when it offers one (e.g. workflows): "off", "offer"
|
||||
# (proposed at the plan gate when available), or "on". Orchestrated runs are
|
||||
# faster wall-clock but spend more tokens.
|
||||
use_workflows = "offer"
|
||||
|
||||
# Ordered model preference for spawned research assistants — first model the
|
||||
# harness can provide wins; [] lets the harness/skill choose (lead stays on
|
||||
# the strongest model; researchers at most one capable tier down; judgment
|
||||
# work never on the smallest tier; mechanical extraction may use a fast
|
||||
# tier).
|
||||
#
|
||||
# Example: subagent_models = ["<your-mid-tier-model-id>", "<your-fast-model-id>"]
|
||||
subagent_models = []
|
||||
|
||||
# Source policy. Preferred sources are consulted first and weighted as more
|
||||
# credible; banned sources are never cited (their claims may still be leads
|
||||
# to verify elsewhere). Entries are domains or plain-text descriptions.
|
||||
# Draft mode writes both policies into drafted prompts.
|
||||
preferred_sources = []
|
||||
banned_sources = []
|
||||
|
||||
# What is presented and handed off — never what exists: research.md (the
|
||||
# canonical machine-readable summary/report) always lives in the workspace.
|
||||
# "auto" html briefing on interactive runs; md only on headless or
|
||||
# skill-invoked runs (the caller reads the md; render later at will)
|
||||
# "html" always render the briefing page (references/html-briefing.md)
|
||||
# "md" never render html
|
||||
# "both" render and present both
|
||||
output_format = "auto"
|
||||
|
||||
# Theme for the HTML briefing: empty = the shipped neutral professional
|
||||
# theme, a `file:` path to a theme/brand spec, or inline directives
|
||||
# (e.g. "Canvas #122543, accent #B66D46, sans-serif, dark-mode aware").
|
||||
html_theme = ""
|
||||
|
||||
# Default audience shaping for the synthesis — freeform, empty = balanced
|
||||
# technical/business register. Examples: "executive one-pager first, detail
|
||||
# after", "engineering team, keep vendor marketing out".
|
||||
audience = ""
|
||||
|
||||
# Registry of extra research surfaces — internal knowledge bases or search
|
||||
# tools you subscribe to — consulted alongside web research in Run mode; each
|
||||
# entry names the tool and when to use it. Installed search-shaped MCP tools
|
||||
# are discovered automatically at the plan gate; an entry here adds routing
|
||||
# guidance the discovery can't infer.
|
||||
#
|
||||
# Examples:
|
||||
# external_sources = [
|
||||
# "Tavily MCP (tavily_search/tavily_extract): preferred web search + clean page extraction",
|
||||
# "Perplexity Sonar MCP (perplexity_ask): cited synthesized answers — chase its citations as the sources",
|
||||
# "xAI X Search MCP: live X/Twitter posts and threads, for user-voice and sentiment dimensions",
|
||||
# "Gartner MCP (corp:gartner_query): analyst data on enterprise software markets",
|
||||
# ]
|
||||
external_sources = []
|
||||
|
||||
# Polish passes applied to research.md at finalize. Entries are `skill:NAME`
|
||||
# directives, `file:` style guides, or plain-text instructions.
|
||||
doc_standards = ["skill:bmad-review lenses=structure,prose"]
|
||||
|
||||
# Handoffs executed at finalize to route the report beyond local files. Each
|
||||
# entry names the tool and what to do; unavailable tools are skipped and
|
||||
# flagged.
|
||||
#
|
||||
# Examples:
|
||||
# "NotebookLM (notebooklm-mcp): create a notebook from research.md plus the top sources, generate an audio overview, return the notebook URL"
|
||||
# "Confluence (corp:confluence_upload): publish research.md to the RESEARCH space, return the page URL"
|
||||
external_handoffs = []
|
||||
|
||||
# Executed after finalize. A string scalar is one instruction; an array is a
|
||||
# sequence. Empty = the run ends with the finalize summary.
|
||||
on_complete = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Research types — subject lenses. Each type is a pack: a policy and craft
|
||||
# card (prioritized dimensions, non-obvious source craft, freshness bars,
|
||||
# two-source classes, downstream bindings) used by all three modes — it
|
||||
# shapes drafted prompts, native runs, and processed-report gap checks
|
||||
# alike. `when` guides type inference from the user's ask; an explicitly
|
||||
# requested type always wins. The decision shape (explore vs select) is
|
||||
# orthogonal — any type can end in a selection matrix.
|
||||
#
|
||||
# Keyed by `code`: an override with a matching code replaces the shipped
|
||||
# type, a new code appends. Empty `pack` disables a type.
|
||||
#
|
||||
# Example (add an org-specific type in team/user override TOML):
|
||||
# [[workflow.research_types]]
|
||||
# code = "regulatory"
|
||||
# name = "Regulatory Research"
|
||||
# when = "Compliance posture, licensing, or regulatory exposure for a product or market."
|
||||
# pack = "file:{project-root}/_bmad/custom/packs/regulatory.md"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "market"
|
||||
name = "Market Research"
|
||||
when = "Market opportunity, customers, competition, sizing, or go-to-market for a product or business decision."
|
||||
pack = "types/market.md"
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "domain"
|
||||
name = "Domain Research"
|
||||
when = "Understanding an industry, sector, or field: structure, players, rules, vocabulary, dynamics."
|
||||
pack = "types/domain.md"
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "technical"
|
||||
name = "Technical Research"
|
||||
when = "A technology area's landscape, patterns, integration approaches, and implementation reality."
|
||||
pack = "types/technical.md"
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "competitive"
|
||||
name = "Competitive Research"
|
||||
when = "Teardown of specific named competitors: offers, pricing, positioning, trajectory, their customers' sentiment."
|
||||
pack = "types/competitive.md"
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "user-voice"
|
||||
name = "User-Voice Research"
|
||||
when = "What users of a product or category actually experience and want: reviews, communities, jobs-to-be-done."
|
||||
pack = "types/user-voice.md"
|
||||
|
||||
[[workflow.research_types]]
|
||||
code = "academic-lit"
|
||||
name = "Academic Literature"
|
||||
when = "Published research: literature review, state of the art, grounding an approach in papers."
|
||||
pack = "types/academic-lit.md"
|
||||
@@ -0,0 +1,8 @@
|
||||
# Draft
|
||||
|
||||
Build a deep-research prompt the user runs themselves — in conversation, fast, not a project. The pack's craft travels inside the prompt so the outside tool works to this harness's standard.
|
||||
|
||||
1. Open the floor before any structured questions: invite the decision they're facing and anything they already have — briefs, links, a prior report, half-formed constraints — in one turn, then ask only what's still missing. Nail the **decision**, topic, and type; load the pack. Ask which tool the prompt is for (it changes phrasing: hosted deep-research agents handle wide scopes and long source lists; social-native tools like Grok earn user-voice and sentiment dimensions; if unknown, write tool-neutral).
|
||||
2. Compose the prompt from the pack: the dimensions as explicit research questions pruned to the decision, the freshness bars as recency requirements, the two-source expectation for its critical claim classes, the audience, the source policy — `{workflow.preferred_sources}` named as sources to prefer, `{workflow.banned_sources}` as sources never to cite — and a **non-negotiable citation demand**: every claim with source URL and publication date, contrary evidence reported, gaps admitted rather than padded. Structure the requested output so Process can extract it cleanly (findings per dimension, a source list).
|
||||
3. Bind `{doc_workspace}`: expand the folder name deterministically (`uv run scripts/recon_kit.py slug "<topic>" --type <type> --pattern "{workflow.run_folder_pattern}"` — same expansion every mode, so the report comes back to the same folder) under `{workflow.research_output_path}`, init the memlog with the decision context, save the prompt as `{doc_workspace}/brief.md`, and present it paste-ready in chat.
|
||||
4. Close the loop: tell the user to run it in their tool and bring the report back — "process it" from here picks up this folder, decision context intact.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Finalize
|
||||
|
||||
Every mode ends here once `research.md` is assembled.
|
||||
|
||||
1. `research.md` is complete per `references/synthesis.md`: decision-first summary, findings, contrary evidence where found, recommendations with downstream bindings, source appendix, staleness map. Frontmatter metadata (`type`, `topic`, `decision`, `source`, `status`, dates) is what lets every downstream consumer trust it without reprocessing.
|
||||
2. **Citation check — mechanical, then semantic.** Run `uv run scripts/recon_kit.py citations {doc_workspace}/research.md` — it diffs inline `[n]` markers against the appendix and lists dangling markers and orphaned rows exactly; fix what it reports. Then a fresh-context subagent does only the judgment half: does each cited source actually say what the text claims? It never rewrites findings — a claim whose source doesn't back it gets its confidence downgraded and the mismatch logged as an `event`.
|
||||
3. Render per `{workflow.output_format}` (see `references/html-briefing.md`): `auto` renders the briefing page on interactive runs, skips on headless/skill-invoked; `html`/`both` always; `md` never. `research.md` always exists — the briefing is its regenerable face.
|
||||
4. Polish: apply each `{workflow.doc_standards}` entry (a `skill:`, `file:`, or plain-text directive) to `research.md`.
|
||||
5. Execute each `{workflow.external_handoffs}` entry (NotebookLM, Confluence, …) — invoke the named tool, surface returned URLs; skip and flag unavailable tools.
|
||||
6. Tell the user what exists and where — report, briefing, imports, memlog — plus what the staleness map says to re-check and when, and that Refresh/Deepen handle it. Invoke `bmad-help` to suggest the next step.
|
||||
7. Run `{workflow.on_complete}` if non-empty — a string is one instruction, an array is a sequence.
|
||||
@@ -0,0 +1,16 @@
|
||||
# HTML Briefing
|
||||
|
||||
Generate `research-briefing.html` in `{doc_workspace}` after `research.md` is final, when `{workflow.output_format}` calls for it: `"auto"` renders on interactive runs and skips on headless/skill-invoked runs (the md is always there to render from later), `"html"`/`"both"` always render, `"md"` never. The page is a full-fidelity presentation of the report, never a second source of truth — same claims, same numbers, same citations; nothing is lost by reading it instead of the markdown.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Self-contained single file**: inline CSS and JS, no external requests of any kind (no CDN, no fonts, no remote images). It must render from a `file://` open, offline, forever.
|
||||
- **Structure**: a header (topic, type, decision, date, depth, verification level) → the executive summary as the opening card → sticky table of contents → dimension sections → contrary evidence (when present) → recommendations → collapsible source appendix → staleness map.
|
||||
- **Confidence is visual**: every claim carries its badge — verified / medium / low / `unverified` / disputed — color-coded with the status text always present (never color alone). Unverified and disputed must be *more* prominent than verified, not less.
|
||||
- **Sources are live**: inline `[n]` markers link to the appendix row; appendix rows link out to the source URL. Source URLs are untrusted content — never hand-escape them: generate the appendix table with `uv run scripts/recon_kit.py escape-sources {doc_workspace}/research.md` and embed its `html` output, which escapes every cell, anchors each row (`id="src-n"`), and links only validated `http(s)` URLs (anything else renders as plain text; the script lists it in `invalid_urls`). Apply the same escape discipline to any other source-derived text you place in attributes.
|
||||
- **Charts sparingly**: only where the data genuinely benefits (market size trajectory, decision matrix scores) — simple inline SVG, labeled axes, no library.
|
||||
- **Responsive and theme-aware**: readable on a phone; respect `prefers-color-scheme` for light/dark.
|
||||
|
||||
## Theme
|
||||
|
||||
`{workflow.html_theme}` governs: a `file:` path loads a theme/brand spec to follow; inline text is applied as directives; empty means the shipped default — neutral, professional, generous whitespace, system font stack, one restrained accent color. Whatever the theme, the confidence-badge semantics above are non-negotiable.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Refresh and Deepen
|
||||
|
||||
Lifecycle intents on an existing run folder.
|
||||
|
||||
## Refresh
|
||||
|
||||
Read `research.md` and `.memlog.md` — never re-research from scratch. Build the refresh set mechanically: assemble the claims (`claim`, `class`, `pub_date`) from the ledger, map the pack's freshness bars to a months-per-class JSON, and run `uv run scripts/recon_kit.py staleness <claims.json> --windows '<map>'` — the stale flags are the candidate set. Confirm it in one exchange, re-verify just those claims, and deliver a **delta report** (confirmed / changed / overturned, new sources) appended to `research.md` with the frontmatter `updated` bumped. Claims outside the set keep their status. An overturned load-bearing claim triggers an explicit warning naming the downstream artifacts that consumed it.
|
||||
|
||||
## Deepen
|
||||
|
||||
Drill into one dimension or add a new one without touching the rest: mini plan gate, acquire → verify for that slice only (or a drafted follow-up prompt when the user's tool is better placed), merge into `research.md`, update only the synthesis sections the new material affects — a deepening that changes no conclusion says so.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Process
|
||||
|
||||
For a report the user names or drops ("there's a research report at <path>, process it"):
|
||||
|
||||
1. **File it.** Find or create the run folder: if a drafted brief for this topic exists, that folder is the target; otherwise infer type and topic from the report (confirm in one line), bind `{doc_workspace}` (expand the folder name with `uv run scripts/recon_kit.py slug` as in Draft), and init the memlog. Move or copy the original into `{doc_workspace}/imports/` untouched — full fidelity is preserved there, and nowhere else.
|
||||
2. **Record provenance** in the memlog: what produced it (which tool or firm), when (ask if not evident — production date drives staleness), and what the user wants decided from it.
|
||||
3. **Extract.** A subagent (fresh context, firewall rules) reads the import and pulls every claim bearing on the decision into digest files under `{doc_workspace}/digests/` — standard shape `{claim, source, publisher, pub_date, accessed, confidence, class}`, keeping the original's citations (the cited source is the publisher; the import is the via). Multiple imports each get their own digest; contradictions between them are findings, not noise.
|
||||
4. **Check against the pack**: which of the type's dimensions the material covers, which are open, where its claims fall inside two-source classes but rest on one publisher. Verification per the resolved `validation` level (`references/verification.md`) — at `normal` this is a spot-check of the load-bearing claims only, minutes not hours.
|
||||
5. **Distill** into `research.md` per `references/synthesis.md` — the succinct, cited, decision-first summary with full metadata frontmatter (topic, type, decision, `source:` provenance, dates, status). This is the artifact downstream skills read; nobody ever reprocesses the import. Open dimensions are listed honestly with a one-line route: draft a follow-up prompt, or a targeted Run on the gap.
|
||||
6. Finalize per `references/finalize.md`.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Run
|
||||
|
||||
Native research, when chosen: resolve effort, hold the plan gate, then run the acquisition loop once per dimension of the approved plan, in plan order.
|
||||
|
||||
## Effort
|
||||
|
||||
Three knobs bundled in a **preset**; any knob pins individually, and **what the user says in the request beats both**.
|
||||
|
||||
| Preset (`{workflow.preset}`) | subagents | sources/round | depth |
|
||||
|---|---|---|---|
|
||||
| `quick` | low (2) | 5 | 1 |
|
||||
| `standard` (default) | normal (3) | 8 | 2 |
|
||||
| `deep` | high (6) | 12 | 3 |
|
||||
|
||||
- **subagents** — parallel assistants: `none` (0 — inline, sequential; also the no-subagent-harness fallback), `low` (2), `normal` (3), `high` (6, cap 10 — beyond the 3–5 sweet spot only for genuinely wide work).
|
||||
- **max_sources_per_round** — distinct sources actually read per dimension per round (cap 25).
|
||||
- **max_depth** — rounds per dimension: initial pass plus lead-following follow-ups (cap 5). A cap, not a quota — dimensions stop early on coverage or novelty exhaustion.
|
||||
- **validation** (orthogonal to preset, default `normal`) — rigor rises `normal` < `high` < `max`; level semantics live in `references/verification.md`. Verification happens per dimension as material lands, never as an end-of-run rewrite pass.
|
||||
|
||||
`{workflow.subagent_models}` is an ordered model preference for assistants — first available wins; empty means harness default. Keep the lead on the strongest model; researchers at most one tier down; judgment work never on the smallest tier.
|
||||
|
||||
## The plan gate
|
||||
|
||||
The one hard stop, kept light: decision, type and pack-derived dimensions pruned to it, shape, the **decomposition topology** — *breadth-first* (independent sub-questions: assistants split the dimensions), *depth-first* (one question that needs several perspectives: assistants split by angle or methodology, not by dimension), or *straightforward* (a focused ask: one assistant, a handful of calls, no fan-out — never overinvest in a simple query) — knobs in force and where each came from, which search surfaces exist (harness web search; installed search-shaped MCP tools; `{workflow.external_sources}` — check, don't assume), whether to run the fan-out as a workflow when the harness offers orchestration and `{workflow.use_workflows}` allows, and an honest time estimate (a standard run is minutes; deep runs are tens of minutes and many times the tokens).
|
||||
|
||||
Present as a compact checklist, get approval, then: bind `{doc_workspace}` under `{workflow.research_output_path}` — expand the folder name with `uv run scripts/recon_kit.py slug "<topic>" --type <type> --pattern "{workflow.run_folder_pattern}"` so the same topic always resolves to the same folder — seed `research.md` from `{workflow.research_template}`, init the memlog (`uv run {project-root}/_bmad/scripts/memlog.py init --workspace {doc_workspace} --field topic="<topic>" --field type="<type>" --field decision="<decision>" --field preset="<preset>"`), log the approved plan as a `decision`, and tell the user the path.
|
||||
|
||||
Each dimension then runs in **rounds** — up to the resolved `max_depth` — and the report grows as material lands: the user watches the document build, not a spinner. Every digest is written to `{doc_workspace}/digests/` the moment it exists — one file per assistant per round (`<dimension>-r<round>-<n>.md`), the digest shape below, raw enough to re-derive from.
|
||||
|
||||
## Rounds and lead-following
|
||||
|
||||
Round 1 pursues the plan's questions **broad-first**: short, wide queries to map what exists, narrowing as the shape emerges — not long specific queries that return nothing. After each round, harvest the leads: new entities worth chasing, unexpected connections, contradictions between sources, and questions the round opened. Contradictions get priority. Promising leads become the next round's brief; note mid-course discoveries in the checkpoint so the user sees the turn happening.
|
||||
|
||||
A dimension stops before its round cap when either holds:
|
||||
|
||||
- **Coverage** — its plan questions are answered, with the critical claims confirmed per the resolved `validation` level.
|
||||
- **Novelty exhaustion** — a full round surfaced no new load-bearing claim or lead.
|
||||
|
||||
Say which one ended it. Hitting the round cap with open questions is reported as an open question, never silently dropped.
|
||||
|
||||
**Stop-and-write valve.** If the run is dragging well past the plan gate's estimate — rounds queuing, budgets mostly spent — stop spawning, synthesize from the digests already on disk, and report the remainder as open questions with a route (a Deepen later, or a drafted prompt for the user's own tool). A shorter honest report beats a longer stale one.
|
||||
|
||||
## The fan-out
|
||||
|
||||
Fan out researcher assistants for the round — concurrency per the resolved `subagents` level, split by the plan's **topology**: breadth-first gives each assistant independent sub-questions; depth-first gives each a distinct perspective or methodology on the *same* question; straightforward is one assistant with a small budget — never fan out what one focused assistant answers. Each assistant runs behind the **research firewall**: it gets its brief and nothing else — no project files, no ambient context. The brief contains:
|
||||
|
||||
- the questions it owns, the decision they serve, and the topic
|
||||
- its search surfaces (specialized tools first — installed search-shaped MCP tools, `{workflow.external_sources}` entries whose directive matches — then generic search), plus `{workflow.preferred_sources}` first / `{workflow.banned_sources}` never
|
||||
- the pack's source craft and freshness bars, and the source-quality card below
|
||||
- its budgets — sources (the round's share of `max_sources_per_round`) and tool calls, scaled to its task: under 5 for a simple lookup, ~5 medium, ~10 hard, 15 for genuinely multi-part, 20 never exceeded. Either budget spent → synthesize what it has
|
||||
- the query craft: short queries (roughly five words or fewer) beat hyper-specific ones that return nothing; broaden when results are sparse, narrow when abundant; never repeat an identical query on the same tool; after every tool result, pause and evaluate — what did this add, what gap remains, what's the best next query — before firing again
|
||||
- the epistemics rules verbatim, and the return contract: a digest, not raw results — findings as claims, each with `{claim, source, publisher, pub_date, accessed, confidence, class}`, plus leads worth chasing and what it looked for and could not find
|
||||
|
||||
**On each return, write the digest to `{doc_workspace}/digests/` before doing anything else with it.**
|
||||
|
||||
Spawn assistants on `{workflow.subagent_models}` when set (first available wins); otherwise the harness default — judgment work never drops to the smallest tier. When subagents are unavailable (or `subagents` is `none`), run the same rounds yourself, sequentially, under the same budgets and the same files-first discipline.
|
||||
|
||||
When workflow orchestration was approved at the plan gate, run the fan-out as a workflow: dimensions as parallel pipelines, assistants returning structured digests. The budgets, digest contract, firewall, and stopping rules apply unchanged — and however the acquisition parallelizes, digests land as files and the lead alone writes `research.md`, committing sections in plan order.
|
||||
|
||||
## Source quality
|
||||
|
||||
One card, applied by every assistant and the lead alike. Prefer **primary sources** — filings, regulator text, official documentation, original papers, a company's own reported numbers — over aggregators and secondary reporting. Red flags that downgrade confidence on sight: speculative language ("could", "may", projections in future tense presented as findings), marketing register, passive voice with unnamed sources, cherry-picked or unsourced numbers, and aggregators recycling a single upstream report (that's one publisher, however many domains echo it). Answer engines (Perplexity Sonar, Grok, and kin) are aggregators too, however good the synthesis: chase their citations and cite those, never the engine. Conflicts resolve by recency, consistency with adjacent established facts, and publisher quality — never by averaging.
|
||||
|
||||
## Synthesize the dimension
|
||||
|
||||
When a dimension's rounds are done:
|
||||
|
||||
1. Verify at landing per `references/verification.md` — at `normal` validation this is a spot-check of the dimension's load-bearing claims, not a sweep.
|
||||
2. Write the dimension's section per the pack's skeleton from its digest files — findings woven into prose answering the dimension's questions, every load-bearing claim cited inline `[n]`, confidence flagged where below high, contradictions reported with both sides cited. Append to `research.md` and add its sources to the running source table.
|
||||
3. Log one memlog line per source batch (`--type source`) and one per load-bearing claim worth tracking for refresh — `--type claim`, text in the machine-readable shape `ref=[n] status=<verified|unverified|disputed|overturned> class=<class> pub=<YYYY-MM> — <claim>` so `scripts/recon_kit.py tally` and `staleness` can read the ledger; a later status change is a fresh claim line with the same `ref=` (last status wins).
|
||||
4. Checkpoint: one or two lines in chat — what the dimension found, anything surprising, anything unresolved. Keep moving unless the user speaks up; a mid-run scope change is logged as a `decision` and the plan adjusts. Headless: skip checkpoints entirely.
|
||||
|
||||
When all dimensions are done, proceed to `references/synthesis.md` for final assembly.
|
||||
@@ -0,0 +1,13 @@
|
||||
# The Select Shape
|
||||
|
||||
When the decision is **choose between candidates** — technologies, vendors, libraries, platforms, agencies, anything — this method layers over whichever research type fits the subject. The type pack still governs sources, craft, and freshness; this shape governs the flow and the verdict.
|
||||
|
||||
1. **Requirements frame.** What must the winner do, under what constraints — scale, compliance, budget, team skills, existing stack, exit-cost tolerance? Split hard gates from weighted preferences and set the weights. Sources: the project itself (brief, PRD, spine, `{workflow.persistent_facts}`, codebase) and the user — web research does not set requirements. **Agree the frame before any candidate research runs**; interactive runs confirm it even though the plan gate approved the dimension list.
|
||||
2. **Candidate screen.** Establish the credible field — leaders, strong challengers, one wildcard — and cut anything failing a hard gate. Screen to 3–5 finalists; record the cuts and why. Screening sources ≤ 6 months old — this field moves.
|
||||
3. **Evidence per criterion.** Score finalists against the frame using the type pack's dimensions and craft, verified against current versions/offerings. Cite every contested cell; where vendor claims and independent experience diverge, the divergence is a finding.
|
||||
4. **Cost & lock-in.** Total cost over the product's horizon — license/subscription, hosting, operational load, learning curve — and the cost of leaving. Current pricing pages read directly (≤ 3 mo, always); pricing-change history — a vendor that repriced once will again; migration-away accounts for real exit costs.
|
||||
5. **Verdict.** The weighted decision matrix — show the scoring, not just totals; a matrix the user can re-weight is worth more than a verdict they must trust. Then: the pick; the named runner-up and the conditions under which it wins instead; the strongest argument against the pick (from the red-team pass when it ran); the cheapest reversibility hedge (abstraction seam, pilot scope, exit test).
|
||||
|
||||
**Two-source classes (added to the type's own):** pricing figures; performance/scale numbers; any cell that decides between the top two finalists.
|
||||
|
||||
**Staleness:** a selection report older than two quarters should be refreshed before anyone acts on it — say so in the report.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Synthesis
|
||||
|
||||
The report answers the decision — whether the material came from a native Run or a processed import. **Succinct is the contract**: findings and verdicts, not essays; rationale lives in the memlog; a reader gets the decision-relevant truth in minutes. For Process mode this is the whole point — the summary is what downstream consumers read so nobody reprocesses the original, and sections with nothing behind them collapse to a line rather than pad.
|
||||
|
||||
Assemble `research.md` in this order, shaped by `{workflow.audience}` and written in `{document_output_language}`:
|
||||
|
||||
1. **Executive summary** — decision-first: what the evidence says to do, the two or three findings that drive that answer, and the biggest caveat. One page maximum, readable standalone. Written last, placed first.
|
||||
2. **Dimension sections** — already written during the loop; now reconciled: consistent terminology, no duplicated ground, verification statuses and any corrections from the pass applied to the text.
|
||||
3. **Cross-dimension insights** — what only the *combination* shows (e.g. the market is growing but the regulatory dimension caps the reachable segment; the technically superior option loses on ecosystem health). This section is the harness earning its keep — if there are no cross-dimension insights, say so rather than manufacture them.
|
||||
4. **Contrary evidence** — when the red-team pass ran and found material; the strongest surviving counter-arguments, cited.
|
||||
5. **Recommendations** — each bound to the decision and, where the project has them, to the downstream artifact that consumes it (per the pack's `Feeds` entries: brief section, PRD input, architecture constraint). Each recommendation names its confidence basis; a recommendation resting on low-confidence or disputed claims says so in the same sentence.
|
||||
6. **Open questions** — what the research could not answer, and what it would take to answer each.
|
||||
7. **Source appendix** — the numbered source table: `[n] | claim/finding it supports | publisher | pub date | accessed | confidence`, the publisher cell a markdown link to the source URL. Every inline `[n]` resolves here.
|
||||
8. **Staleness map** — the claims that age fastest, computed not hand-derived: build the claims list (`claim`, `class`, `pub_date`) from the ledger, map the pack's freshness bars to months per class, and run `uv run scripts/recon_kit.py staleness <claims.json> --windows '<map>'` — render its re-check dates and close by noting the earliest. This is Refresh's work order.
|
||||
|
||||
Update the frontmatter (`status: complete`, `updated`, and the verified/unverified counts from `uv run scripts/recon_kit.py tally {doc_workspace}/.memlog.md` — never hand-counted), log a final `event` in the memlog, and proceed to `references/finalize.md`.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Verification
|
||||
|
||||
The trust layer — the same rules whatever produced the material (a native run's digests or a processed import). Verification happens **as material lands**, per dimension, in fresh-context verifier subagents reading digest files — never as an end-of-run rewrite pass over an hour of accumulated context. Late-pass rewrites degrade reports; landing-time checks improve them.
|
||||
|
||||
## The claims ledger
|
||||
|
||||
The memlog `claim` entries are the ledger: every claim a decision could rest on, with its class (each pack names its classes — quantitative sizes, pricing, versions/compatibility, regulatory assertions, …), source, publisher, publication date, and status. New claims enter `unverified`; on a Refresh or Deepen run, claims outside the run's scope keep their prior status from the memlog — only new and in-scope claims are (re)checked.
|
||||
|
||||
## Levels
|
||||
|
||||
Per the resolved `validation` level (request > knob > default `normal`):
|
||||
|
||||
- **normal** — spot-check the **load-bearing claims only**: the handful per dimension the recommendation actually rests on. One independent-source check each, at landing. Everything else ships with its single source cited and confidence marked honestly. Fast by design.
|
||||
- **high** — cross-check every claim in the pack's *two-source classes*, and run the red-team pass on major conclusions regardless of `{workflow.red_team}`.
|
||||
- **max** — cross-check every ledger claim, run the red-team pass below at full breadth (every major conclusion), and primary-source-priority ranking: where a primary source (filing, regulator text, official docs, original paper) exists, secondary reporting alone does not verify.
|
||||
|
||||
Verifier assistants run behind the research firewall on `{workflow.subagent_models}` when set; judgment work never drops to the smallest tier.
|
||||
|
||||
**Independent** means a different publisher with different underlying data or reporting — not a syndication, quote, or republication of the first source, and not the same vendor's marketing in two places. An imported report counts as one publisher regardless of how many sources it cites internally; two imports from different tools agreeing is genuine confirmation, and their disagreement is a finding.
|
||||
|
||||
Outcomes per claim: **verified** (independent source agrees within tolerance — for quantitative claims, same order of magnitude and direction), **disputed** (independent sources materially disagree — report both figures, both cited; never average), **unverified** (no independent check within budget — the claim stays, flagged, and joins the staleness map), or **overturned** (the weight of evidence contradicts it — corrected in the text, original noted). Every status change lands in the memlog as a fresh `claim` line with the same `ref=` and the new status — last status wins, which is how `scripts/recon_kit.py tally` reads the ledger. A verification outcome adjusts status and flags — it never licenses rewriting a finding's substance beyond what the new evidence says.
|
||||
|
||||
Confidence rendered in the report: **high** (verified, fresh, credible publishers), **medium** (single credible source, fresh), **low** (stale, weak publisher, or disputed) — plus the explicit `unverified` flag. Confidence is per-claim, never per-section.
|
||||
|
||||
## Red-team pass
|
||||
|
||||
The single adversarial mechanism — no other verifier duplicates it. Off by default (`{workflow.red_team}` = `"off"`; `"offer"` proposes it at the plan gate, `"on"` always runs; `high` validation includes it for major conclusions, `max` runs it at full breadth). When it runs: for each major conclusion, a **fresh-context** skeptic subagent — the conclusion and a search budget, no supporting evidence, no run context — hunts for disconfirming evidence: the bear case, failed attempts, contrary data, the strongest good-faith argument the conclusion is wrong.
|
||||
|
||||
What comes back is weighed, not appended: a conclusion that survives gets its strongest counter-argument acknowledged in the synthesis; one that doesn't is revised before the report states it. Material findings land in a **Contrary Evidence** section with full citation discipline. Zero findings after a real search is itself reportable — say what was searched for and not found.
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# ///
|
||||
"""recon_kit — deterministic helpers for bmad-deep-recon.
|
||||
|
||||
The mechanical half of the research workflow: everything here is exact,
|
||||
repeatable work the LLM should never re-derive by hand. All subcommands
|
||||
print one JSON object to stdout; diagnostics go to stderr. Exit codes:
|
||||
0 = pass, 1 = findings that need attention, 2 = usage/parse error.
|
||||
|
||||
Subcommands:
|
||||
citations RESEARCH_MD
|
||||
Cross-check inline [n] markers against the source-appendix table:
|
||||
dangling markers (no appendix row) and orphaned rows (never cited).
|
||||
tally MEMLOG_MD
|
||||
Count memlog entries by type, and claim entries by status.
|
||||
Claim lines carry `status=<word>` and optionally `ref=[n]`; for a
|
||||
given ref the LAST status wins, so status changes are appends.
|
||||
staleness CLAIMS_JSON --windows JSON [--today YYYY-MM-DD]
|
||||
Given claims [{claim, class, pub_date}] and a months-per-class map
|
||||
(e.g. '{"size/growth": 18, "pricing": 3}'), compute each claim's
|
||||
re-check date, flag stale ones, and report the earliest re-check.
|
||||
slug TOPIC --type TYPE [--pattern P] [--date YYYY-MM-DD]
|
||||
Expand the run-folder pattern deterministically so the same topic
|
||||
always lands in the same folder across draft -> process -> refresh.
|
||||
escape-sources RESEARCH_MD
|
||||
Emit the source-appendix table as HTML with every cell escaped and
|
||||
only validated http(s) URLs turned into links, for the briefing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
MARKER_RE = re.compile(r"\[(\d+)\](?!\()") # [3] but not a [3](url) link
|
||||
MD_LINK_RE = re.compile(r"\[([^\]]*)\]\((\S+?)\)")
|
||||
BARE_URL_RE = re.compile(r"https?://[^\s|)\]]+")
|
||||
|
||||
|
||||
def out(payload: dict, exit_code: int) -> int:
|
||||
print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
|
||||
return exit_code
|
||||
|
||||
|
||||
def read_text(path_arg: str) -> str:
|
||||
if path_arg == "-":
|
||||
return sys.stdin.read()
|
||||
return Path(path_arg).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def strip_fences(text: str) -> str:
|
||||
"""Blank out fenced code blocks so their contents never count as markers or rows."""
|
||||
lines, fenced = [], False
|
||||
for ln in text.splitlines():
|
||||
if ln.lstrip().startswith("```"):
|
||||
fenced = not fenced
|
||||
lines.append("")
|
||||
continue
|
||||
lines.append("" if fenced else ln)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def table_cells(line: str) -> list[str]:
|
||||
return [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
|
||||
|
||||
def appendix_rows(text: str) -> dict[int, list[str]]:
|
||||
"""Source-appendix rows: markdown table rows whose first cell is a bare [n] / n."""
|
||||
rows: dict[int, list[str]] = {}
|
||||
for ln in text.splitlines():
|
||||
stripped = ln.strip()
|
||||
if not stripped.startswith("|"):
|
||||
continue
|
||||
cells = table_cells(stripped)
|
||||
if not cells or len(cells) < 2:
|
||||
continue
|
||||
m = re.fullmatch(r"\[?(\d+)\]?", cells[0])
|
||||
if m:
|
||||
rows[int(m.group(1))] = cells
|
||||
return rows
|
||||
|
||||
|
||||
# --- citations ---------------------------------------------------------------
|
||||
|
||||
def cmd_citations(args) -> int:
|
||||
text = strip_fences(read_text(args.file))
|
||||
rows = appendix_rows(text)
|
||||
markers: set[int] = set()
|
||||
for ln in text.splitlines():
|
||||
stripped = ln.strip()
|
||||
if stripped.startswith("|"):
|
||||
cells = table_cells(stripped)
|
||||
if cells and re.fullmatch(r"\[?(\d+)\]?", cells[0]):
|
||||
continue # an appendix row is not a citation of itself
|
||||
markers.update(int(n) for n in MARKER_RE.findall(ln))
|
||||
dangling = sorted(markers - set(rows))
|
||||
orphaned = sorted(set(rows) - markers)
|
||||
ok = not dangling and not orphaned
|
||||
return out({
|
||||
"markers": sorted(markers),
|
||||
"appendix_rows": sorted(rows),
|
||||
"dangling_markers": dangling,
|
||||
"orphaned_rows": orphaned,
|
||||
"ok": ok,
|
||||
}, 0 if ok else 1)
|
||||
|
||||
|
||||
# --- tally -------------------------------------------------------------------
|
||||
|
||||
ENTRY_RE = re.compile(r"^- (?:\(([\w-]+)(?: by [^)]*)?\)\s*)?(.*)$")
|
||||
|
||||
|
||||
def cmd_tally(args) -> int:
|
||||
text = read_text(args.file)
|
||||
body = text.split("---", 2)[-1] if text.startswith("---") else text
|
||||
by_type: dict[str, int] = {}
|
||||
by_ref: dict[int, str] = {}
|
||||
unref_status: dict[str, int] = {}
|
||||
entries = 0
|
||||
for ln in body.splitlines():
|
||||
m = ENTRY_RE.match(ln)
|
||||
if not m or not ln.startswith("- "):
|
||||
continue
|
||||
entries += 1
|
||||
etype = m.group(1) or "note"
|
||||
by_type[etype] = by_type.get(etype, 0) + 1
|
||||
if etype == "claim":
|
||||
status_m = re.search(r"status=([\w-]+)", m.group(2))
|
||||
status = status_m.group(1) if status_m else "unknown"
|
||||
ref_m = re.search(r"ref=\[?(\d+)\]?", m.group(2))
|
||||
if ref_m:
|
||||
by_ref[int(ref_m.group(1))] = status # last status wins per ref
|
||||
else:
|
||||
unref_status[status] = unref_status.get(status, 0) + 1
|
||||
claims: dict[str, int] = dict(unref_status)
|
||||
for status in by_ref.values():
|
||||
claims[status] = claims.get(status, 0) + 1
|
||||
return out({
|
||||
"entries": entries,
|
||||
"by_type": dict(sorted(by_type.items())),
|
||||
"claims": dict(sorted(claims.items())),
|
||||
"claims_total": sum(claims.values()),
|
||||
}, 0)
|
||||
|
||||
|
||||
# --- staleness ---------------------------------------------------------------
|
||||
|
||||
def parse_date(raw: str) -> date:
|
||||
raw = raw.strip()
|
||||
for fmt in ("%Y-%m-%d", "%Y-%m", "%Y"):
|
||||
try:
|
||||
return datetime.strptime(raw, fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError(f"unparseable date: {raw!r} (want YYYY[-MM[-DD]])")
|
||||
|
||||
|
||||
def add_months(d: date, months: int) -> date:
|
||||
total = d.month - 1 + months
|
||||
year, month = d.year + total // 12, total % 12 + 1
|
||||
return date(year, month, min(d.day, calendar.monthrange(year, month)[1]))
|
||||
|
||||
|
||||
def cmd_staleness(args) -> int:
|
||||
try:
|
||||
payload = json.loads(read_text(args.file))
|
||||
windows = {k.lower(): int(v) for k, v in json.loads(args.windows).items()}
|
||||
today = parse_date(args.today) if args.today else date.today()
|
||||
except (ValueError, json.JSONDecodeError) as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
return 2
|
||||
claims = payload["claims"] if isinstance(payload, dict) else payload
|
||||
results, no_window, stale_count = [], set(), 0
|
||||
earliest: date | None = None
|
||||
for c in claims:
|
||||
cls = str(c.get("class", "")).lower()
|
||||
try:
|
||||
pub = parse_date(str(c["pub_date"]))
|
||||
except (KeyError, ValueError) as e:
|
||||
print(f"error in claim {c!r}: {e}", file=sys.stderr)
|
||||
return 2
|
||||
months = windows.get(cls)
|
||||
if months is None:
|
||||
no_window.add(cls)
|
||||
results.append({**c, "recheck": None, "stale": None})
|
||||
continue
|
||||
recheck = add_months(pub, months)
|
||||
stale = recheck <= today
|
||||
stale_count += stale
|
||||
earliest = recheck if earliest is None or recheck < earliest else earliest
|
||||
results.append({**c, "recheck": recheck.isoformat(), "stale": stale})
|
||||
return out({
|
||||
"today": today.isoformat(),
|
||||
"claims": results,
|
||||
"stale_count": stale_count,
|
||||
"earliest_recheck": earliest.isoformat() if earliest else None,
|
||||
"no_window_classes": sorted(no_window),
|
||||
}, 1 if stale_count else 0)
|
||||
|
||||
|
||||
# --- slug --------------------------------------------------------------------
|
||||
|
||||
def slugify(text: str, max_len: int = 40) -> str:
|
||||
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
|
||||
text = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
||||
return re.sub(r"-{2,}", "-", text)[:max_len].rstrip("-")
|
||||
|
||||
|
||||
def cmd_slug(args) -> int:
|
||||
slug = slugify(args.topic)
|
||||
if not slug:
|
||||
print("error: topic slugified to an empty string", file=sys.stderr)
|
||||
return 2
|
||||
folder = (args.pattern
|
||||
.replace("{research_type}", args.type)
|
||||
.replace("{topic_slug}", slug)
|
||||
.replace("{date}", args.date or date.today().isoformat()))
|
||||
return out({"topic_slug": slug, "folder": folder}, 0)
|
||||
|
||||
|
||||
# --- escape-sources ----------------------------------------------------------
|
||||
|
||||
def safe_url(raw: str) -> str | None:
|
||||
parsed = urlparse(raw)
|
||||
return raw if parsed.scheme in ("http", "https") and parsed.netloc else None
|
||||
|
||||
|
||||
def cell_html(cell: str, invalid: list[str]) -> str:
|
||||
"""Escape a cell; a markdown link or bare URL becomes an <a> only when http(s)."""
|
||||
link = MD_LINK_RE.search(cell)
|
||||
if link:
|
||||
url = safe_url(link.group(2))
|
||||
label = html.escape(link.group(1) or link.group(2))
|
||||
if url:
|
||||
return html.escape(cell[:link.start()]) + \
|
||||
f'<a href="{html.escape(url, quote=True)}" target="_blank" rel="noopener">{label}</a>' + \
|
||||
html.escape(cell[link.end():])
|
||||
invalid.append(link.group(2))
|
||||
return html.escape(cell.replace(link.group(0), link.group(1) or link.group(2)))
|
||||
bare = BARE_URL_RE.search(cell)
|
||||
if bare:
|
||||
url = safe_url(bare.group(0))
|
||||
if url:
|
||||
escaped = html.escape(url, quote=True)
|
||||
return html.escape(cell[:bare.start()]) + \
|
||||
f'<a href="{escaped}" target="_blank" rel="noopener">{escaped}</a>' + \
|
||||
html.escape(cell[bare.end():])
|
||||
invalid.append(bare.group(0))
|
||||
return html.escape(cell)
|
||||
|
||||
|
||||
def cmd_escape_sources(args) -> int:
|
||||
text = strip_fences(read_text(args.file))
|
||||
rows = appendix_rows(text)
|
||||
if not rows:
|
||||
print("error: no source-appendix table rows found", file=sys.stderr)
|
||||
return 2
|
||||
invalid: list[str] = []
|
||||
body_rows = []
|
||||
for n in sorted(rows):
|
||||
cells = rows[n]
|
||||
tds = "".join(f"<td>{cell_html(c, invalid)}</td>" for c in cells[1:])
|
||||
body_rows.append(f'<tr id="src-{n}"><td>[{n}]</td>{tds}</tr>')
|
||||
table = ('<table class="sources"><tbody>' + "".join(body_rows) + "</tbody></table>")
|
||||
return out({"rows": len(rows), "invalid_urls": invalid, "html": table},
|
||||
1 if invalid else 0)
|
||||
|
||||
|
||||
# --- entry point -------------------------------------------------------------
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pc = sub.add_parser("citations", help="cross-check [n] markers vs the source appendix")
|
||||
pc.add_argument("file", help="path to research.md (or - for stdin)")
|
||||
pc.set_defaults(func=cmd_citations)
|
||||
|
||||
pt = sub.add_parser("tally", help="count memlog entries by type and claims by status")
|
||||
pt.add_argument("file", help="path to .memlog.md (or - for stdin)")
|
||||
pt.set_defaults(func=cmd_tally)
|
||||
|
||||
ps = sub.add_parser("staleness", help="compute re-check dates from freshness windows")
|
||||
ps.add_argument("file", help="claims JSON: [{claim, class, pub_date}] (or - for stdin)")
|
||||
ps.add_argument("--windows", required=True,
|
||||
help='JSON months-per-class map, e.g. \'{"pricing": 3}\'')
|
||||
ps.add_argument("--today", help="override today's date (YYYY-MM-DD)")
|
||||
ps.set_defaults(func=cmd_staleness)
|
||||
|
||||
pg = sub.add_parser("slug", help="expand the run-folder pattern deterministically")
|
||||
pg.add_argument("topic", help="research topic text")
|
||||
pg.add_argument("--type", required=True, help="research type code (e.g. market)")
|
||||
pg.add_argument("--pattern", default="{research_type}-{topic_slug}-{date}",
|
||||
help="folder pattern (default: {research_type}-{topic_slug}-{date})")
|
||||
pg.add_argument("--date", help="override date (YYYY-MM-DD; default today)")
|
||||
pg.set_defaults(func=cmd_slug)
|
||||
|
||||
pe = sub.add_parser("escape-sources",
|
||||
help="source appendix as escaped HTML with validated links")
|
||||
pe.add_argument("file", help="path to research.md (or - for stdin)")
|
||||
pe.set_defaults(func=cmd_escape_sources)
|
||||
|
||||
args = p.parse_args(argv)
|
||||
try:
|
||||
return args.func(args)
|
||||
except FileNotFoundError as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# ///
|
||||
"""Tests for recon_kit.py."""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from recon_kit import add_months, main, parse_date, slugify
|
||||
|
||||
REPORT = """---
|
||||
title: 'market research: widgets'
|
||||
---
|
||||
|
||||
# Report
|
||||
|
||||
The market is growing fast [1] and pricing clusters at $20 [2].
|
||||
An uncited assertion sits here [4].
|
||||
|
||||
```
|
||||
[9] inside a fence never counts
|
||||
```
|
||||
|
||||
## Source appendix
|
||||
|
||||
| [n] | Supports | Publisher | Pub date | Accessed | Confidence |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| [1] | market growth | [Gartner](https://example.com/g) | 2026-01 | 2026-07-01 | high |
|
||||
| [2] | pricing | [Acme](javascript:alert(1)) | 2026-05 | 2026-07-01 | medium |
|
||||
| [3] | never cited | https://example.com/x | 2025-11 | 2026-07-01 | low |
|
||||
"""
|
||||
|
||||
MEMLOG = """---
|
||||
topic: widgets
|
||||
updated: 2026-07-22T10:00
|
||||
---
|
||||
|
||||
- (decision) plan approved
|
||||
- (source) round 1 batch: 4 sources
|
||||
- (claim) ref=[1] status=verified class=size/growth pub=2026-01 — market growing 12% CAGR
|
||||
- (claim) ref=[2] status=unverified class=pricing pub=2026-05 — pricing clusters at $20
|
||||
- (claim) ref=[2] status=verified class=pricing pub=2026-05 — confirmed by second source
|
||||
- (claim) status=unverified class=behavior pub=2025-03 — users churn at day 8
|
||||
- (event) dimension 1 complete
|
||||
"""
|
||||
|
||||
|
||||
def run(argv):
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
code = main(argv)
|
||||
return code, json.loads(buf.getvalue())
|
||||
|
||||
|
||||
class CitationsTest(unittest.TestCase):
|
||||
def test_cross_check(self):
|
||||
report = Path(__file__).parent / "_report.md"
|
||||
report.write_text(REPORT, encoding="utf-8")
|
||||
try:
|
||||
code, result = run(["citations", str(report)])
|
||||
finally:
|
||||
report.unlink()
|
||||
self.assertEqual(result["dangling_markers"], [4])
|
||||
self.assertEqual(result["orphaned_rows"], [3])
|
||||
self.assertNotIn(9, result["markers"]) # fenced content ignored
|
||||
self.assertEqual(code, 1)
|
||||
|
||||
|
||||
class TallyTest(unittest.TestCase):
|
||||
def test_last_status_wins_per_ref(self):
|
||||
log = Path(__file__).parent / "_memlog.md"
|
||||
log.write_text(MEMLOG, encoding="utf-8")
|
||||
try:
|
||||
code, result = run(["tally", str(log)])
|
||||
finally:
|
||||
log.unlink()
|
||||
self.assertEqual(result["by_type"]["claim"], 4)
|
||||
self.assertEqual(result["claims"], {"unverified": 1, "verified": 2})
|
||||
self.assertEqual(result["claims_total"], 3) # ref=[2] counted once
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
|
||||
class StalenessTest(unittest.TestCase):
|
||||
def test_dates(self):
|
||||
self.assertEqual(parse_date("2026-01"), date(2026, 1, 1))
|
||||
self.assertEqual(add_months(date(2026, 1, 31), 1), date(2026, 2, 28))
|
||||
|
||||
def test_windows(self):
|
||||
claims = json.dumps([
|
||||
{"claim": "sizing", "class": "size/growth", "pub_date": "2024-06"},
|
||||
{"claim": "pricing", "class": "pricing", "pub_date": "2026-06"},
|
||||
{"claim": "odd", "class": "unmapped", "pub_date": "2026-06"},
|
||||
])
|
||||
f = Path(__file__).parent / "_claims.json"
|
||||
f.write_text(claims, encoding="utf-8")
|
||||
try:
|
||||
code, result = run([
|
||||
"staleness", str(f),
|
||||
"--windows", '{"size/growth": 18, "pricing": 3}',
|
||||
"--today", "2026-07-22",
|
||||
])
|
||||
finally:
|
||||
f.unlink()
|
||||
self.assertEqual(result["stale_count"], 1) # sizing recheck 2025-12 < today
|
||||
self.assertEqual(result["earliest_recheck"], "2025-12-01")
|
||||
self.assertEqual(result["no_window_classes"], ["unmapped"])
|
||||
self.assertEqual(code, 1)
|
||||
|
||||
|
||||
class SlugTest(unittest.TestCase):
|
||||
def test_deterministic_folder(self):
|
||||
self.assertEqual(slugify("Créme Brûlée: AI Tools!"), "creme-brulee-ai-tools")
|
||||
code, result = run(["slug", "SMB Accounting SaaS", "--type", "market",
|
||||
"--date", "2026-07-22"])
|
||||
self.assertEqual(result["folder"], "market-smb-accounting-saas-2026-07-22")
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
|
||||
class EscapeSourcesTest(unittest.TestCase):
|
||||
def test_escaping_and_url_validation(self):
|
||||
report = Path(__file__).parent / "_report.md"
|
||||
report.write_text(REPORT, encoding="utf-8")
|
||||
try:
|
||||
code, result = run(["escape-sources", str(report)])
|
||||
finally:
|
||||
report.unlink()
|
||||
self.assertEqual(result["rows"], 3)
|
||||
self.assertTrue(any(u.startswith("javascript:") for u in result["invalid_urls"]))
|
||||
self.assertNotIn("javascript:", result["html"]) # never linked
|
||||
self.assertIn('href="https://example.com/g"', result["html"])
|
||||
self.assertIn('id="src-1"', result["html"])
|
||||
self.assertEqual(code, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,19 @@
|
||||
# Academic Literature Pack
|
||||
|
||||
Serves: ground an approach in published research, scan the state of the art, run a defensible literature review, cite properly in technical writing.
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. The canon — seminal papers and the best recent surveys of the area
|
||||
2. State of the art — current best results, benchmarks, and how they're measured
|
||||
3. Methods & limitations — what the leading approaches assume and where they break
|
||||
4. Open problems & live debates — what the field disagrees about right now
|
||||
5. Who works on this — the labs and groups whose output to watch
|
||||
|
||||
**Craft (the non-obvious):** find one good survey before reading twenty abstracts; chase citations both directions — who they cite and who cites them (Semantic Scholar/Google Scholar); label preprint vs peer-reviewed on every citation — arXiv is not acceptance; take benchmark numbers from the original paper, never from a competitor's comparison table; check retraction and replication status on any load-bearing empirical claim; a result only ever shown by one lab is a lead, not a fact.
|
||||
|
||||
**Freshness:** state-of-the-art claims ≤ 12 mo (ML ≤ 6 mo) · seminal work has no freshness bar — but check whether it was since superseded.
|
||||
|
||||
**Two-source classes:** any empirical claim a conclusion rests on — independent replication or corroboration, not the same lab twice.
|
||||
|
||||
**Feeds (bmm):** technical and architecture bets · content and writing that cites · build-vs-adopt judgments on research-grade techniques.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Competitive Research Pack
|
||||
|
||||
Serves: position against *named* competitors, build battlecards, sharpen differentiation, anticipate their next move. (Surveying an unnamed field is the market type; this pack is for teardowns of specific players.)
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. Offer & feature teardown — what they actually ship, tried directly where possible
|
||||
2. Pricing & packaging — models, tiers, what changed recently and which direction
|
||||
3. Positioning & messaging — who they claim to serve, the story they tell, the gap between claim and product
|
||||
4. Trajectory — funding, hiring, release cadence: where they're headed
|
||||
5. Their customers' voice — what users of *their* product praise and complain about
|
||||
|
||||
**Craft (the non-obvious):** their changelog and release notes are roadmap truth; job postings reveal strategy six months early; their customers' 1–3★ reviews are your wedge; archived pricing pages (Wayback) show pricing direction, not just position; sales-facing comparison pages overclaim — verify capability claims against their docs; try the product yourself when a trial exists — an hour in-product beats ten reviews.
|
||||
|
||||
**Freshness:** pricing & features ≤ 3 mo · trajectory signals ≤ 6 mo · customer sentiment ≤ 12 mo.
|
||||
|
||||
**Two-source classes:** traction and market-share claims; any capability claim of theirs that your differentiation rests on.
|
||||
|
||||
**Feeds (bmm):** brief (alternatives) · PRD (differentiation) · GTM battlecards and positioning.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Domain Research Pack
|
||||
|
||||
Serves: commit to building in an industry, talk credibly with domain experts, scope a product for a regulated field, brief a team entering unfamiliar territory.
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. Industry structure & value chain — how value flows, who captures margin where
|
||||
2. Key players & gatekeepers — incumbents, platforms, whose APIs/standards/marketplaces you build with or against
|
||||
3. Rules of the game — laws, licenses, de-facto standards, what compliance costs a new entrant
|
||||
4. Language & mental models — the vocabulary and implicit workflows practitioners think in; **build the glossary — it's why domain research exists**
|
||||
5. Technology adoption — the current technical baseline and where the industry sits on the adoption curve
|
||||
|
||||
**Craft (the non-obvious):** annual-report industry sections are free structured teardowns; conference keynotes reveal who actually matters; go to regulator sites directly for any load-bearing claim — and read enforcement actions to learn what's actually punished versus merely written; job postings name the real tools and skills; pending regulatory changes matter as much as current text.
|
||||
|
||||
**Freshness:** structure ≤ 3 yr · player landscape ≤ 18 mo · regulatory status: verify current on every load-bearing claim, whatever its date · tech adoption ≤ 18 mo, AI-adoption claims ≤ 6 mo.
|
||||
|
||||
**Two-source classes:** regulatory and compliance assertions; quantitative industry figures a recommendation rests on; claims about a gatekeeper's policy.
|
||||
|
||||
**Feeds (bmm):** brief (context, feasibility) · PRD (constraints, domain vocabulary) · architecture (integration landscape, compliance requirements).
|
||||
@@ -0,0 +1,19 @@
|
||||
# Market Research Pack
|
||||
|
||||
Serves: enter or skip a market, position a product, pick a segment, price an offer, pitch investors.
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. Market size & growth — the *reachable* market, not the headline TAM
|
||||
2. Customer segments & behavior — who buys, deciding how, valuing what
|
||||
3. Pain points & unmet needs — what they complain about, work around, pay to avoid
|
||||
4. Competitive landscape — who competes for this budget, including substitutes and "do nothing"
|
||||
5. GTM & pricing dynamics — channels, sales motion, accepted pricing models, realistic CAC
|
||||
|
||||
**Craft (the non-obvious):** 1–3★ reviews are the gold for pains; public-company 10-K/S-1 industry sections are free analyst-grade sizing; read competitor pricing pages directly, never roundups; funding history + job postings reveal competitor trajectory; a complaint pattern persisting across years is a stronger finding, not a stale one.
|
||||
|
||||
**Freshness:** size/growth ≤ 18 mo · pricing & feature claims ≤ 3 mo · behavior data ≤ 2 yr · GTM benchmarks ≤ 12 mo.
|
||||
|
||||
**Two-source classes:** market size and growth figures; any quantitative claim a recommendation rests on; competitor traction claims.
|
||||
|
||||
**Feeds (bmm):** brief (opportunity, problem, users) · PRD (personas, differentiation) · pricing and GTM decisions.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Technical Research Pack
|
||||
|
||||
Serves: adopt a technology area, design an integration approach, ground an architecture in current practice, assess feasibility before committing a roadmap.
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. Landscape & maturity — dominant approaches, what's consolidating vs churning, what the current generation newly makes possible
|
||||
2. Integration & interoperability — protocols, formats, auth patterns, where integrations actually hurt
|
||||
3. Architecture patterns in practice — which named patterns dominate at what scale, and what the failures teach
|
||||
4. Implementation reality — learning curve, tooling, operational burden, what teams say 6–12 months in
|
||||
5. Ecosystem health — contributor/release vitality, backing durability, the five-year regret risk
|
||||
|
||||
**Craft (the non-obvious):** read the retrospective threads, not the launch threads; favor accounts with production numbers over advocacy; before citing a pain point, check whether it was since fixed — an old complaint against a current version is a false claim; read repository metrics over time, never snapshots; issue trackers reveal the gap between docs and reality.
|
||||
|
||||
**Freshness:** versions & compatibility ≤ 1 mo · ecosystem signals ≤ 6 mo · landscape ≤ 12 mo (AI-adjacent ≤ 3 mo) · patterns ≤ 2 yr.
|
||||
|
||||
**Two-source classes:** version/compatibility claims; performance or scale numbers a recommendation rests on; claims that a technology or pattern failed — one post-mortem is an anecdote.
|
||||
|
||||
**Feeds (bmm):** architecture spine (candidate paradigms, operational constraints) · brief (feasibility) · roadmap risk and estimates.
|
||||
@@ -0,0 +1,19 @@
|
||||
# User-Voice Research Pack
|
||||
|
||||
Serves: understand what users of a product or category actually experience and want — personas, jobs-to-be-done, requirements grounded in evidence rather than assumption.
|
||||
|
||||
**Dimensions (priority order — prune to the decision):**
|
||||
|
||||
1. Who they are & their jobs-to-be-done — the progress they're hiring the product to make
|
||||
2. Complaint & workaround patterns — where current options fail them
|
||||
3. Delight & switching triggers — why they stay, what made them move
|
||||
4. Unmet needs & requests — what they ask for, and the deeper need under the ask
|
||||
5. Their language — the words users say, versus the words vendors use
|
||||
|
||||
**Craft (the non-obvious):** mine 1–3★ reviews for pain *and* 5★ for why they stay; a workaround is unpriced demand — someone laboring around a gap has already voted; forums, Discord, and Reddit surface what surveys miss — people lie less when nobody's asking — but they over-sample the loud, so triangulate against reviews and any survey data; keep verbatim quotes, redacted — user words carry evidence paraphrase destroys, but usernames, handles, emails, and identifying links never enter the report or memlog; feature-request boards measure willingness to wait, not willingness to pay; distinguish loud power-users from the silent majority — count distinct voices, not thread length.
|
||||
|
||||
**Freshness:** sentiment ≤ 18 mo · complaints re-checked against the current version before citing.
|
||||
|
||||
**Two-source classes:** any prevalence claim ("most users…", "the top complaint is…") — two independent communities, not two threads in the same one.
|
||||
|
||||
**Feeds (bmm):** PRD (personas, requirements rationale) · UX research inputs · brief (problem) · product copy in the users' own language.
|
||||
Reference in New Issue
Block a user