| name | context-mill |
| description | Use when rolling a feature out to the next SDK platform ("do it like we did for iOS, but for Android"), identifying which doc articles a dev task or MR affects, deepening a zone's brief through an interview, or looking up where ground truth for a platform's API actually lives. |
Context Mill — Zoned Docs Knowledge Map
Context for docs work lives in .claude/context-mill/, organized into zones — areas of the docs
with shared judgment (who reads it, what's internal, what ripples) — rather than per-article facts.
Files and ownership
| File | Owner | Carries |
|---|
docs-map.jsonl | script | Mechanical per-document facts: title, headings, symbols, sidebars, links, three narrow hashes (content_hash, shape_hash, api_hash). Regenerated by npm run mill. Never hand-edit. |
zones/<zone>.md | human + agent | Zone brief — judgment about one area: what it is, sources of truth, what's in/out of scope, reader jobs, ripple rules, boundaries, ticket language, gaps and misses. One to two pages. |
zones.json | human via agent | Machine layer: article id → zone/role/audience, plus the zone list (id, title, kind). Edited only via mill:assign / mill:new-zone, never by hand. |
platforms.md | human | Per-platform facts: ground-truth source id, current SDK version state, local quirks, the release-branch scope rule. |
scope.md | human | Corpus-wide scope rules — what earns a doc, what doesn't, depth and duplication policy, which claims need evidence first. A zone brief's What we document, what we don't carries only its delta from this file. Read it before deciding how much of a change to write up. |
sources.md | human | Catalog of ground-truth code/spec sources (repo path, remote, default ref, branch pattern). |
rollouts/<slug>.md | agent, reviewed by human | State of one feature rolling across platforms: canon decided on the first platform, a per-platform table, open questions. |
.zone-state.json | script | Snapshot of member hashes at the last mill:reviewed <zone>, used to compute drift. Never hand-edit. |
Routing rule: anything the script can derive from the map, a human never writes. In a zone brief,
the roster lives inside <!-- mill:auto:roster --> … <!-- /mill:auto --> markers and is rewritten
by npm run mill on every run — never edit inside those markers. Everything outside them is judgment
the script must never touch.
Disproving a mechanism is not disproving the claim. The commonest way this project has produced a
confident falsehood is: pick the mechanism you assume is behind a claim, grep for it, find nothing,
declare the claim false. A worked example that cost two wrong edits — "observer mode causes
cantMakePayments" was removed after grepping notAllowedInObserveMode and finding it only in error
definitions. The real path never mentions that name: purchaser is constructed only when observer mode
is off, and makePurchase throws cantMakePayments when it is nil. The docs had been right; the grep
tested a hypothesis and was reported as testing the claim. Before writing "X is not the cause", state
which mechanism you looked for and ask what else could produce the same symptom — a nil collaborator,
a guard clause, a mapping table, a config branch. If you cannot enumerate the alternatives, you have a
hypothesis, not a finding.
Gaps and misses is a claim, not a finding — verify before repeating it. A gap entry says something
is absent, and absence is the one thing a brief cannot establish by reading its own zone: the answer is
often written up in a neighbouring zone under a framing nobody thought to search. This has already
happened twice on the same fact — two briefs asserted that revoke-durability was undocumented while
test-purchases-in-sandbox documented it in a table. So when you write a gap, state how you
established the absence (the exact grep, the zones checked) and when; and when you read one, re-check
it before telling anyone the docs don't cover something. Prefer "documented only as X, not for Y" over
"undocumented" — mislocated is far more common than missing, and the two need different fixes.
What the dangling-id check actually looks at, so you don't mangle formatting to appease it. It scans
backticked tokens in three sections only — Reader jobs, Ripple rules, Ticket language — and it
considers a token only if it is lowercase, hyphenated, and free of dots, slashes, capitals and
underscores. So api-adapty/operations/updatePaywall, adapty-api.yaml, price_usd and
Adapty.activate() are all invisible to it and keep their backticks. Never strip code formatting from a
path, a spec id or a field name out of fear of the validator — write it the way it reads best. The cost of
that narrowness is that ids containing a dot or underscore go unchecked (the four spec ids, plus
initial_ios and run_stop_ab_tests); those six are verified by hand.
The map holds API specs, not only articles. Every OpenAPI spec registered in
src/api-reference/config.json is a record too, zoned like anything else, with kind: "spec" and an
id that keeps its extension — adapty-api.yaml, web-api.yaml. Its headings are URL paths and its
symbols are operationIds, so api_hash moves when an endpoint is renamed. What this means for a
task: for the server-side and web APIs the spec is the reference, so an endpoint or field change is
an edit to the YAML, not to a hand-written object article. The server-side-api and other-apis
briefs name which articles are unmaintained; read them before touching anything API-shaped.
Execution modes
Who the agent can talk to while doing any of the modes below. Mode detection is declared, never
inferred — read it before doing anything else:
echo "${MILL_MODE:-interactive}"
An absent or unrecognized value means interactive.
| Mode | Question goes to | May it ask? |
|---|
interactive (default) | the user, synchronously | Yes. |
assisted-async | the Slack thread that launched the run | Yes, but never blocking — post and finish. |
unattended | the draft PR body | No — record an assumption instead (references/judgment-rules.md). |
In unattended mode, do not attempt to ask a question at all — write the assumption down and keep
going.
Mode 1 — Lookup
Zone-first, not grep-first.
npm run mill, then npm run mill:status. Read the per-zone drift report.
- Pick the one or two zones the task lands in. If
mill:status says zones.json not present, stop
here and follow "Bootstrap state" at the end of this file — the remaining steps need zones.
- Read those zone briefs whole — a brief is one to two pages, no chunked retrieval.
- If the task's wording still doesn't match anything, translate it before searching. Two files do this,
and they divide by scope:
aliases.md carries synonyms that hold across the whole corpus (Flow ↔
Paywall Builder, access level ↔ entitlement), while a zone brief's Ticket language carries how
tickets phrase requests inside that one area, mapped to the article that answers them — usually with
the cause attached, which is what makes the lookup land rather than merely get close. Check the zone's
table first, aliases.md when the vocabulary problem spans areas. Neither repeats the other, so a
miss in one is not evidence of a miss in the other.
- Use
docs-map.jsonl to reach the actual articles the brief's roster names.
- Read article bodies only for candidates the brief cannot settle on its own.
- Report a checklist: each affected article, its role, and which section(s) likely change. In an
sdk-matrix zone, use the roster's family rows to name each platform's counterpart article.
Never report a missing per-platform article as a gap. Reviewing coverage is not this tool's job:
not every topic needs an equivalent on all seven platforms, and article names vary in wording, so an
empty roster cell means "nothing under this family key", never "missing".
An empty section is unwritten, not settled. Twelve zones are still stubs, and a stub's judgment
sections are blank. A blank What we document, what we don't means nobody has written the rule down
yet — never that the zone has no rules, and never that the topic is undocumented. Same for a blank
Sources of truth: it does not mean the claim has no source, only that no one recorded which one.
When a section you need is blank, fall back to steps 5–6 — the map, then the article bodies — and name
the blank section in your report, so Mode 3 fills it once instead of the next agent guessing at it again.
Do not grep src/content/docs until steps 1–5 are exhausted: the map plus the briefs already carry
titles, headings, symbols, and the task-language synonyms ("Ticket language" section) that a raw grep
misses.
Mode 2 — Rollout (the primary workflow)
Carrying one feature from a shipped platform to the next.
- Find the rollout file at
.claude/context-mill/rollouts/<slug>.md, or scaffold one with
npm run mill:new-rollout <slug>. The scaffold already contains all four required sections and a
row per platform — never hand-write one, or it will drift from the format the validator checks.
Identify the source platform (the most recent row marked shipped) and the target platform from
the "Platform state" table.
npm run mill:refs to propose a target code branch, using sources.md's ref_pattern. Several
candidates → confirm with the user (interactive) or apply references/judgment-rules.md (other
modes). If it prints No .claude/context-mill/sources.md yet, that is a missing data layer, not a
transient failure — the message is the signal, not the exit code (it exits 0, same as any other
unauthored-layer state): do not retry, do not clone anything, do not guess a branch. Ask the
user for the target's repo path, remote, default_ref, and ref_pattern so they can add the
sources.md entry, and stop until it exists.
- Read the source platform's docs diff:
git show <docs commit> using the rollout table's
docs commit column. This is the canon — read it, don't reconstruct intent from the finished
article.
- Read the target's code diff only:
git diff <default_ref>...origin/<branch>. For React Native
and Capacitor, follow the sources.md rule into the shared jscore clone — their public API is
defined there, not in the platform repo.
- Check
platforms.md for the target's quirks, and the zone's "What we document, what we don't" for
the filter against internal detail in the diff.
- Check the migration-guide criteria below; write the guide if they're met.
- Write docs for the target platform only — never another platform's docs in the same pass.
- Update the rollout file's platform table (branch, code/docs status, articles written, docs commit,
PR). Append anything learned the hard way to the zone's "Gaps and misses".
Never check out. No checkout, switch, pull, stash, or merge in another repository.
Read with git show <ref>:<path> and git diff a...b only. The user's working tree in the SDK repo
is never touched. This also keeps diff as the default lens: a diff is what separates a
user-facing change from an internal reshuffle, which is the filter against documenting internal
plumbing.
Report every discrepancy between the target's code diff and the source platform's canon as an explicit
list — e.g. "Android has a parameter the iOS guide doesn't mention: new feature, or did we miss it on
iOS?" Never resolve one silently.
Mode 3 — Interview
Deepens one zone's brief.
- Read the brief. State plainly what it cannot answer.
- Ask the user 5–10 targeted questions: who the reader is, where truth lives, what's internal here,
what ripples when this changes.
- Write the answers into the brief's judgment sections (not the roster).
- Run
npm run mill:reviewed <zone-id> to stamp it reviewed.
Mode 4 — Autolog
Run this at the end of any task that touched a zone — writing an article, fixing a defect, reading
code to answer a question. It is how the briefs stay current, and it is the one mode that pays for itself
on work you were doing anyway: you have just been in the code, which is exactly when the zone's claims
are cheapest to test.
Treat the brief as a hypothesis and the code as the check. The most valuable thing this mode produces
is not new prose — it is finding that the brief is wrong. That happened repeatedly on 2026-08-11/12: a
brief said the store never writes prices back to Adapty (there is a second price table), another put a
UTC claim in doubt that turned out correct, another asserted three claims about Adapty Mail that a
five-week-old checkout had made stale.
Report, don't silently edit
Produce a diff against the brief, in your final report to the user:
- What you learned from the source that the brief does not say.
- What the brief says that you found to be wrong — lead with this.
- What you could not verify, and which mechanism you looked for.
Then edit the brief only where both hold: you have command output that settles it, and the edit is
dated with the evidence named. Everything else stays a proposal for the owner.
Why the asymmetry: a wrong brief is worse than a missing one, because it is authoritative — it becomes the
context every later agent trusts without re-checking. And if agents append to briefs freely during article
work, every zone sits in drafted, unreviewed forever and that state stops distinguishing anything.
Never
- Never run
mill:reviewed. That is the owner's act of saying "I read this and it is right", not a
side effect of touching a zone.
- Never write an absence you have not tested — put the grep in the text, and prefer "documented only
as X" over "undocumented". Mislocated is far more common than missing.
- Never delete or contradict a claim you merely failed to confirm. Say you could not confirm it, and
name the mechanism you looked for.
Mechanical parts that need no permission
Run npm run mill. The roster re-renders and sources: follows from what the sections cite — both are
script-owned, so there is no judgment to defer. What you must not touch by hand is anything between the
mill:auto markers.
Judgment rules
Read references/judgment-rules.md before taking any decision alone in rollout mode. It carries the
decision table (what may be decided alone, what escalates), the assumption ledger, and the stop rule
at three accumulated assumptions.
Migration guides
Write one when the developer must edit code on the target platform: a public API is removed or
renamed, a signature changes, a method/callback becomes required, a default behavior changes, the
minimum platform/toolchain rises, or the installation method changes. Don't write one for an additive
optional API with a sensible default, an internal refactor, or a deprecation while the old path still
works — note it in the live article instead.
Whenever you write one, do all four — the last three are the steps that get forgotten:
- Name the file
migration-to-<platform>-sdk-<version>.mdx, title it Migrate Adapty <Platform> SDK to v<version>, and never rename an existing guide that uses one of the older schemes (SEO).
- Put it in the platform folder (
src/content/docs/ios/), not version-3.0/.
- Register it on the platform hub page (
<platform>-sdk-migration-guides — all seven exist).
- Add the doc id to the platform sidebar JSON under SDK migration guide.
Full house rules (the three guide forms, the known orphans) belong in the sdk-migrations zone brief.
That brief does not exist yet — until it does, match the existing guides under
src/content/docs/*/migration-to-*, and record what you learn in that zone's "Gaps and misses" once
the brief is scaffolded.
Commands and maintenance
| Command | Does |
|---|
npm run mill | Rebuilds docs-map.jsonl; re-renders every mill:auto roster block in the briefs. |
npm run mill:status | Per-zone drift report plus validation errors. Non-zero exit means look, not that something is unrecoverable — it is not a deploy gate. |
npm run mill:refs | Fetches every local-clone source; reports local branch, lag, and branches matching ref_pattern. Never writes to a working tree. |
npm run mill:reviewed <zone> | Stamps a brief's reviewed_shape/reviewed_at and snapshots member hashes. |
npm run mill:assign <article> <zone> -- --role=<role> --audience=<a,b> | Assigns one article to a zone. Add --family=<id> for a member of an sdk-matrix zone and --version=<v> for a version-matrix zone — without them the article gets its own roster row instead of sitting beside its counterparts, so "which article covers this on Android" stops working for that topic. |
npm run mill:new-zone <zone> -- --kind=flat|sdk-matrix|version-matrix --title="…" | Adds a zone and scaffolds a stub brief. |
npm run mill:new-rollout <slug> | Scaffolds rollouts/<slug>.md with all four required sections and one row per platform. |
Three maintenance operations, most to least frequent:
- New article in an existing zone — assignment is part of writing the article, not separate
housekeeping: run
mill:assign, then mill and mill:status, as the last step of the doc-author /
editor workflow.
- New zone —
mill:new-zone, move article assignments over, run mill to render the roster, fill
the judgment sections through interview mode, then mill:reviewed. Update the neighbouring zones'
"Boundaries" section too — the split changes their answer to "how is this zone different", and only
the new zone gets remembered otherwise.
- Renaming, merging, splitting zones — rare and disruptive: reassign articles, delete/create
briefs, reset
reviewed_shape on all affected zones, update "Boundaries" on both sides, drop the
removed zone from .zone-state.json, and fix zones: references in rollout files.
State of the data layer
Bootstrapped as of 2026-08-10, recounted 2026-08-12. zones.json (35 zones), zones/ (35 briefs),
sources.md and platforms.md all exist, the partition is complete, and mill:status prints a real
per-zone drift report — so every mode above works as written and there is no fallback path to take.
Two things are worth knowing rather than rediscovering:
rollouts/ does not exist yet. Nothing is broken: mill:new-rollout <slug> creates the directory
on first use, and mill:status treats an absent rollouts directory as a state, not an error. The first
real cross-platform rollout authors the first file.
- Brief depth is uneven, and
mill:status says which is which — read it, don't assume from this
list. As of 2026-08-12: 4 zones carry a reviewed_at (3 of those 4 already show
reviewed_shape out of date, meaning a human vouched for them and the corpus has moved since),
19 are drafted, unreviewed, and 12 are stubs — ab-tests, access-levels, ai-advisory,
app-and-account-settings, flutterflow, migration-from-competitors, onboardings-legacy,
other-apis, retention-messaging, server-side-api, subscribers-and-profiles, web-payments.
A stub scaffolded by mill:new-zone has the five section headings, a rendered roster, and nothing
else — no Ticket language table, no prose. So a stub is a real answer for routing and no answer at
all for judgment: it tells you which articles are in scope and stays silent on everything else. A
shallow lookup in a stub zone is the signal to deepen it through interview mode, never evidence that
the zone is wrong or that the topic is undocumented — see "An empty section is unwritten, not settled"
in Mode 1.
The Phase 1 layers this skill used before zoning are gone: docs-enrichment.jsonl was retired
2026-08-10 and its curated search terms now live in the briefs' Ticket language sections. aliases.md
was deliberately kept — see the scope division in Mode 1, step 4.