/voc-synthesize — Cross-Source Pattern Detection
One mention is an anecdote. Three mentions across three different channels is a pattern. That distinction is the entire product, and this skill is built so it cannot be quietly relaxed.
Clustering is a judgment task, so subagents do it. Counting is not, so code does it. The
confirmation rule lives in scripts/synthesize.js reading config/thresholds.json, and nowhere
else. If you find yourself reasoning about whether something "feels confirmed," you have left the
rule behind.
Requires
| What | Value |
|---|---|
| Input | outputs/voc/profiled/*.json, plus the call analysis and review briefs |
| Config | config/thresholds.json |
| Cost | Subagent tokens. Five clustering agents per run. |
Phase 1 — Gather
Collect everything in the window (default from windows.default_days):
| Source | Path |
|---|---|
| Profiled signals | outputs/voc/profiled/*.json |
| Call intelligence | outputs/calls/analysis/call-intelligence-*.md |
| Review briefs | outputs/voc/sources/reviews/brief-*.json |
| Chat, notes, CRM, surveys, tickets | outputs/voc/sources/<channel>/* |
Do not read the quarantine folder. Tier-U signals are excluded by design.
Report which sources were empty. A source that returned nothing because its connector is broken looks identical to a source where nothing happened, and the difference matters — say which you believe it was and why.
Phase 2 — Cluster in parallel
Spawn one subagent per dimension, all in a single message:
pain · request · praise · objection · competitive
Each subagent receives the full corpus and returns clusters for its dimension only:
Group signals in the corpus below into themes for the "<dimension>" dimension.
Cluster on underlying meaning, not wording. "Export is slow", "downloads time out"
and "can't get data out for big events" are ONE theme. Err toward merging: two
themes that should have been one is a worse error than one theme that should have
been two, because splitting hides frequency.
Return ONLY this JSON:
{"clusters":[{
"theme": "short specific name, written as the customer would recognize it",
"dimension": "<dimension>",
"signals": [{"channel":"...","weight_tier":"...","company":"...",
"date":"YYYY-MM-DD","verbatim":"exact quote","source_file":"..."}]
}]}
Rules:
- Copy verbatim quotes EXACTLY. Do not clean up grammar or trim for length.
- Every signal keeps its original channel and weight_tier. Do not reassign them.
- Do not invent signals to strengthen a cluster.
- A signal may appear in only one cluster within your dimension.
- If a quote is ambiguous between two themes, put it in the more specific one.
CORPUS:
[...]
Merge all five results into one clusters.json.
Phase 3 — Apply the rule in code
node scripts/synthesize.js outputs/voc/synthesis/clusters.json
This writes <date>-confirmed.json, <date>-watchlist.json, <date>-dropped.json and prints
the tally. It handles four things you should not re-derive by hand:
- Independent source means distinct channel. Two calls are one source.
- Tier U never counts toward confirmation, but is reported per theme so you can see how much unattributed volume sits behind it.
- One tier-A signal alone never confirms. Seniority is not corroboration.
- A theme where every mention comes from one account gets downgraded, because one loud customer is not a market pattern. This is the check most VoC tooling omits, and it is the one that most often prevents a bad roadmap decision.
Take the script's output as fact. Do not re-rank it because a dropped theme seemed interesting.
Phase 4 — Write the report
outputs/voc/synthesis/<date>-synthesis.md:
# VoC Synthesis — week of <range>
## Coverage
| Channel | Signals | Status |
|---|---|---|
| Calls | N | ok |
| Reviews | N | ok |
| Chat | 0 | connector not configured |
...
**Quarantined (unattributed):** N signals excluded from all counts.
## Confirmed themes
### <Theme name>
**High confidence** · N mentions · N channels (call, review, chat) · N accounts · tiers A:1 B:4
**First seen** YYYY-MM-DD · **Most recent** YYYY-MM-DD
> "verbatim quote"
> — Company, YYYY-MM-DD (call, tier A)
<3-5 quotes>
**What this means:** <one sentence, concrete, naming who should do what>
## Medium confidence
<same shape>
## Watchlist
<single-channel themes, one line each, with the channel named>
## What did not confirm
<count, plus two or three notable near-misses and what they were short of>
Lead with the confirmed themes. Put coverage above them so nobody reads a thin week as a complete picture.
What this gets wrong
- The rule is a heuristic, not a truth test. Three channels can all reflect one loud market segment. Check the account spread the script reports before acting.
- Merging hides nuance. Aggressive clustering is right for finding frequency and wrong for finding the specific edge case that is actually blocking deals. Read the raw quotes.
- Absence proves nothing. A theme that did not confirm may simply be one nobody asked about. Your discovery script shapes this corpus more than you think.
- Recency bias is built in. A weekly window favours whatever happened this week. Run a quarterly window occasionally to see what has been steadily true.
- The rule can be gamed by adding connectors. Wiring up more channels makes confirmation
easier, not truer. If you add sources, consider raising
min_independent_sources.