| id | 222 |
|---|---|
| title | Single source of truth for distribution channels |
| status | ✅ |
| summary | Hold every distribution channel — push, pull, and toolchain — in one schema-bound directory, and generate every channel list from it: the install guide table, the release pipeline table, the README, the feature card, and an interactive website picker. A `sync-channels --check` gate fails CI when any surface drifts. |
| model | opus |
| depends-on |
Adding or retiring a distribution channel should be a one-file edit. Every list of channels — across the docs, the README, and the website — derives from that one source. Drift fails CI instead of shipping.
mdsmith ships through a growing set of channels, and they are enumerated by hand in at least five places. The copies have already drifted:
- The channels table in install.md is currently complete (Go, npm, npx, PyPI, uvx, pipx, Homebrew, mise, asdf, GitHub release) but hand-maintained.
- The "Installs everywhere" card in features/index.md and the install-everywhere.md page both omit Homebrew.
- The README install block omits Homebrew and still flags asdf as "pending", though the explicit-URL asdf install works today.
- The homepage install tabs (
install:in _index.md) are a curated five-tab subset (go, npm, pip, vs code, claude) with neither asdf nor Homebrew.
Only one surface is generated. The publishing table in
release.md renders a
<?catalog?> over
release-channels/.
That directory holds only the five push channels.
Those are the ones a CI job publishes with a credential
(npm, PyPI, Open VSX, Visual Studio Marketplace, GitHub
Releases).
The other channels have no file at all. The pull
channels read from a release: Homebrew via the
notify-homebrew-tap job, asdf via
jeduden/asdf-mdsmith, and mise via the ubi backend.
The toolchain channels are go install and the
npx / uvx / pipx runners. Adding one updates only
the doc the author remembered. That push/pull split is
the root cause of the drift.
Two precedents already exist in this repo:
- plan/210 did this
for product messaging: one schema-bound source,
<?include?>/<?catalog?>for the Markdown surfaces, and amdsmith-release sync-messaging --checkdrift gate for the non-Markdown surfaces. - The
npmPlatformBuildsarray in buildnpm.go, gated byTestNpmChannelDocMatchesPlatformBuilds, is the precedent for a Go-list ↔ doc drift test.
This plan generalizes the first precedent from messaging to channels.
Make the release-channels/ directory the single source of truth for every channel. Keep the directory name. A rename would ripple across .mdsmith.yml, the release docs, and the Go release tests. Each channel stays one file.
Extend proto.md. The publish-only fields become optional. These new discriminator fields are added:
mechanism: "push" | "pull" | "toolchain".artifact: "cli" | "vscode-extension" | "claude-plugin".command— the install one-liner.audience— the "Best for" cell.platforms— optional tags (macos,linux,windows,node,python,go,editor) that drive the interactive picker's filters.registry,credential,job— now optional, set only onmechanism: pushchannels.
The implementation dropped a planned status: live | pending field: every listed command works today, so the
field was uniformly live. The short-form asdf and mise
caveat (plan/145)
stays as body prose on those channel files instead.
Add the seven missing files: go.md, npx.md,
uvx.md, pipx.md, homebrew.md, asdf.md,
mise.md. The five push files already exist.
Extending the schema edits proto.md (the referenced
schema file), not .mdsmith.yml
itself. If the kind's path-pattern or
kind-assignment must change, that needs explicit
maintainer consent per CLAUDE.md.
Two generators already exist, and each fits a
different shape. This plan uses both, and leans on
extract harder than the catalog-only first draft.
<?catalog?> aggregates many files into rows. It reads
frontmatter only. So it stays the tool for the
cross-file tables in install.md and release.md.
mdsmith extract projects one schema-bound file into a
typed tree of frontmatter plus body sections. Its
read-side, <?include ... extract:?>, splices one
typed leaf back into Markdown with no fragment file
(plan/211). Three
surfaces should use it:
- Typed channel bodies. Give each channel file a section schema. The body then carries structured detail: per-platform commands, verify steps, and the artifact list. This is the "deeper levels" payoff.
- Per-channel install sections. Each
##section in install.md pulls its own command from the channel file with<?include ... extract: command ?>. The prose can no longer drift from the source. - The machine data file.
website/data/channels.yamlis the picker's data source. The sync command loads each channel throughmdsmith extract, then marshals the projected fields into a flat array (renamingchannelurltourl, dropping the publish-only fields). The picker reads that file.
extract is single-file, so the set stays one file per
channel. That also keeps the catalog tables and the
per-channel website pages working. The sync command
loads each file through extract, the way
sync-messaging already does, and assembles the array.
- The install.md table
becomes a
<?catalog?>over the directory, sorted byweight, withcommandandaudiencecolumns. Each per-channel section below the table embeds its own command with<?include ... extract: command ?>. - The release.md
table becomes a
<?catalog?>withwhere: mechanism == "push", so Homebrew, asdf, and mise are excluded by filter rather than by omission. - The feature card and
install-everywhere.md
pull a generated channel-name fragment via
<?include?>, so the prose cannot drift. - A new
website/data/channels.yamlis assembled by the sync command below (extract per file, marshalled to YAML). The homepage install section reads it through the picker, which replaces the old hand-curatedinstall:tabs in _index.md — so no second list is left to keep in sync.
Add a Hugo partial install-picker.html to the install
page. It reads website/data/channels.yaml. It filters
channels by OS and packaging community, using the platforms
tags. It then shows the matching command with a copy
button, reusing the .install-row styling in
app.css. A picker keeps
every channel visible as the list grows past a flat
table.
Add a sync-channels command to the release tool,
modeled on sync-messaging. It loads each channel file
through mdsmith extract. It writes the Hugo data file
from that typed output, which the picker reads.
The --check flag exits non-zero on drift. Wire it into
ci.yml next to the
sync-messaging --check step. The Markdown tables stay
current through mdsmith fix and the catalog-drift
check.
- Phase 1 — docs SSOT. Generalize
proto.md, add the seven channel files, and convert install.md, release.md, and the feature card to generated output. No Go or website changes. - Phase 2 — sync and gate. Add
sync-channels --check,website/data/channels.yaml, and the CI gate. - Phase 3 — interactive. Add the Hugo install picker.
- Extend
proto.md:
make
registry/credential/joboptional and addmechanism,artifact,command,audience,platforms. Backfill the five existing push files somdsmith check .stays green. - Add
go.md,npx.md,uvx.md,pipx.md,homebrew.md,asdf.md, andmise.mdwith full frontmatter. - Convert the
install.md channels
table to a
<?catalog?>; runmdsmith fixand confirm the rendered table matches today's content. (Deferred: embedding each per-channel command with<?include ... extract: command ?>— the per-channel sections show commands in fenced code blocks, where the directive does not run, and the typed-value include lands in plan/211.) - Convert the
release.md table to
a
where: mechanism == "push"catalog. - (Deferred.) The feature card,
install-everywhere.md,
and the README install block stay curated prose —
corrected by hand to list Homebrew and asdf. A
<?catalog?>emits a block list, not the inline sentence these blurbs use, so generating them needs a join-style row template, tracked separately. The authoritative lists (the two tables and the picker data) are generated; these blurbs reference a subset. - Add
mdsmith-release sync-channelsplus unit tests under internal/release/, following thesync-messagingpattern. It loads each file viamdsmith extractand writeswebsite/data/channels.yamlfrom the typed output. - Add
sync-channels --check, wire it into ci.yml, and document it in release-tooling.md. - Add the
install-picker.htmlpartial and styling, and point the install page at it.
- Every channel is one file under the release-channels/ directory; the install table, the release table, and the picker data all derive from it.
-
mdsmith fixregenerates the install.md and release.md tables from the source, and is byte-stable on a second run. -
mdsmith extractprojects each channel file's frontmatter, andwebsite/data/channels.yamlis that output, sorted by weight. - The README, the feature card, and install-everywhere.md list Homebrew and asdf, and no surface calls asdf "pending".
-
mdsmith-release sync-channelsis byte-stable on a second run;--checkexits non-zero on drift and is enforced in ci.yml. - The website renders an interactive picker that filters channels by platform tag (templates parse-checked; a Hugo build still has to confirm the rendering).
- All tests pass:
go test ./.... -
go tool golangci-lint runreports no issues.
- The asdf and mise registry submissions themselves (plan/145).
- The six Claude Code plugins — they are an editor surface, not a binary channel, and stay as prose in install.md.
- Translating channel copy into other languages.