Split from the original match-structure (GH-291, 2026-07): the
section-level driver (compare, blueprint extraction, section-by-section
rewrite) moved to match-outline. This skill retains the quantitative
metrics engine, corpus aggregation, frequency analysis, and the similarity
guard that other prose skills import.
Match structure (paragraph/sentence-level metrics)
This skill provides the quantitative style measurement layer that other
prose skills depend on. It profiles markdown papers at the sentence and
paragraph level — distributions, passive voice, hedging, citation density,
word and phrase frequencies, stock idiom usage — and aggregates those into
a corpus profile. It also provides the similarity plagiarism guard used by
match-outline's rewrite mode and match-voice's verify step.
Where things live
- Corpus:
<db-dir>/papers/*.md— the markdown conversions fetched byupdate-references, selected via entries inreferences.yaml. Default selection is entries withstatus: summarized; pass--alltostyle.py corpusto include every entry with anmd_path. - Quantitative profile:
<db-dir>/voice-profile.json, written bystyle.py corpus. Regenerate only when the corpus changes. - Voice anchors: passage-level tf-idf retrieval from
writing-voice/exemplars, viavoice_anchors.py. - Venue profiles:
writing-voice/venues/<name>.yaml— per-venue parameter bundles (anchor query, blueprint, targets, tell lexicon, gates) consumed by humanize/filter-tells/tighten-style. Schema in thewriting-voicerepository rule; loader/validator isvenue_profile.py.
Running the scripts
RUN="pixi run --manifest-path <skill>/../../pixi.toml python"
style.py subcommands
$RUN <skill>/scripts/style.py --db <db-path> profile <paper.md> # one paper, full JSON
$RUN <skill>/scripts/style.py --db <db-path> corpus # aggregate, write voice-profile.json
$RUN <skill>/scripts/style.py --db <db-path> compare <draft.md> # metric deltas vs corpus
$RUN <skill>/scripts/style.py freq <paper.md> # frequency tables only
$RUN <skill>/scripts/style.py similarity <file> --against <sources> [--baseline <draft>]
$RUN <skill>/scripts/style.py burstiness <draft.md> [--baseline <before.md>] [--per-paragraph] [--text]
burstiness — sentence-length dispersion
profile already carries every burstiness field; burstiness is the compact
view of the same numbers, for the places a report wants one line rather than a
page of JSON. It prints sentence count, mean, stdev, CV, min, max, median,
p10, p90, and a word-length histogram.
CV — stdev over mean — is the scale-free form, and the one to quote. Stdev alone conflates dispersion with register: a paper averaging 28-word sentences carries a larger stdev than a newsletter averaging 14 without being any less uniform. The percentiles and the histogram say where the variance sits, which is what a rewrite pass needs, since a document can hit a CV target by growing one 60-word sentence and that is not the same prose as one that alternates.
--baseline <before.md> adds a delta block over every scalar, which is the
before/after column in the humanize report. --per-paragraph adds a row per
multi-sentence paragraph so a rewrite can find the flat stretches instead of
reshaping prose that already varies. --text renders one line per document
instead of JSON.
Which fields aggregate: sentence_length_cv, _median, _p10, and _p90
are in METRIC_KEYS, so they appear in corpus profiles, compare deltas, and
venue targets. Min and max are not — a sample's extrema move with sample
size rather than with style, and averaging them across a corpus produces a
number that means nothing. The histogram is a dict and does not average at
all.
filter-tells reports its own sentence_length_cv in the structural scan. It
runs slightly lower than this one, because that script drops sentence
fragments under four words and those are exactly the ones that widen the
spread. For a before/after comparison, keep both sides on one tool.
voice_anchors.py subcommands
$RUN <skill>/scripts/voice_anchors.py discover <file>
$RUN <skill>/scripts/voice_anchors.py profile [--voice-dir D | --for file] [--force]
$RUN <skill>/scripts/voice_anchors.py anchors --text <file>|- [--for file] [-k N] [--role R]
venue_profile.py subcommands
$RUN <skill>/scripts/venue_profile.py discover <file> # find venues/ via walk-up
$RUN <skill>/scripts/venue_profile.py list --for <file> # available venue names
$RUN <skill>/scripts/venue_profile.py show --venue N --for <file> # validated profile JSON
$RUN <skill>/scripts/venue_profile.py validate <profile.yaml>
$RUN <skill>/scripts/venue_profile.py bootstrap --voice-dir D --tags a,b [--role R] [--stratum pre-ai]
$RUN <skill>/scripts/venue_profile.py bootstrap --voice-dir D --tags a,b --venue N --write
$RUN <skill>/scripts/venue_profile.py set-anchors --venue N --voice-dir D --arm "tags~clipped" [--composite X]
Python consumers import venue_profile.resolve(start_path=..., venue=...),
which raises on schema errors — a broken profile is refused, never partially
applied.
bootstrap (GH-339) measures a corpus slice — a manifest query
(role/tags/stratum) or an explicit --files list — with the same metric
engine as style.py corpus, and emits the profile's
targets/targets_std/targets_provenance block; --write merges it into
an existing venues/<name>.yaml (author the profile first). Profiles are
measured, never hand-written. set-anchors records an anchor query into a
profile with provenance — from tune-anchors arm expressions (repeatable
--arm, parsed by tune-anchors' own parser so the syntax cannot drift) or
from explicit --role/--tags/--stratum/--author flags.
Consumers
- match-outline — imports
stylefor corpus selection, the similarity guard, and theCITATION_REpattern. Usesgpt-oss:120b-cloudvia Ollama for whole-document structural rewriting. - match-voice — imports
style.similarity_reportfor the rewrite verification gate; importsvoice_anchorsfor anchor retrieval. - filter-tells — consumes
voice-profile.jsonas a detector input (--voice-profile=<path>); usesvoice_anchors.pyfor anchor retrieval and baseline profiling. - tighten-style — imports
stylefor nominalization density metrics. - tune-anchors — imports
voice_anchorsfor the anchor tuning workflow.
Dependencies
style.py is stdlib-only except PyYAML. voice_anchors.py uses PyYAML.
No model calls — this is the deterministic half of the analysis pipeline.