Skip to content

Commit 2413d9e

Browse files
author
merge-queue-bot
committed
Merge PR #502: benchmark: one honest snapshot for every perf claim; fix the apparent slowdown
2 parents 26df529 + 07bb5a4 commit 2413d9e

30 files changed

Lines changed: 1607 additions & 573 deletions

.claude/skills/docs-author/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ adapted to mdsmith.
210210
feature list. Paste what the tool actually
211211
prints; never fabricate output.
212212
4. **One concrete number, not adjectives.** mise:
213-
"900+ tools, 1 toml file". mdsmith: "523 files
214-
in ~0.2 s, ~10x Node markdownlint", linked to a
215-
reproducible benchmark.
213+
"900+ tools, 1 toml file". mdsmith: "checks its
214+
Markdown in ~0.5 s, ~10x faster than Node
215+
markdownlint", linked to a reproducible benchmark.
216216
5. **Short pitch, then detail.** Three to five
217217
benefit-named groups, one line each, before any
218218
long card list. Never open with the full

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ row: "- [{summary}](../{filename})"
8787
- [`mdsmith lsp` emits diagnostics, quick-fixes, and navigation — definition, references, symbol search, and a call-hierarchy over `<?include?>`, `<?catalog?>`, and cross-file links — consumed by any LSP-aware editor.](../docs/features/live-diagnostics.md)
8888
- [`mdsmith extract` projects a schema-conformant Markdown file into a JSON, YAML, or msgpack data tree; `mdsmith export` writes a portable, directive-free copy that renders anywhere.](../docs/features/markdown-as-data.md)
8989
- [Pin a Markdown convention to get a curated rule preset and a target renderer flavor in one switch. `MDS034` flags syntax the flavor will not render; a placeholder vocabulary spares template tokens.](../docs/features/markdown-conventions.md)
90-
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — roughly 4x faster than Node markdownlint, with a CI gate against regression.](../docs/features/performance.md)
90+
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — an order of magnitude faster than Node markdownlint, with a CI gate against regression.](../docs/features/performance.md)
9191
- [CI badge, Go Report Card grade, and Codecov coverage badge report live project health. mdsmith lints its own docs with the rules it ships, and a coverage gate blocks any merge that drops below the line.](../docs/features/quality.md)
9292
- [`mdsmith list query 'status: "✅"' plan/` selects files by a CUE expression on front matter; `mdsmith metrics rank` ranks files by any shared metric — both ready to pipe into a release script.](../docs/features/release-gating.md)
9393
- [Rename a heading and every workspace anchor link that points at it is rewritten in one atomic edit. Link-reference labels rename with their uses. A colliding slug fails loudly instead of silently breaking cross-file links.](../docs/features/rename.md)

.github/workflows/benchmark.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ name: Cross-tool benchmark
55
# pattern): both the refreshed numbers (data + fragments) and
66
# the benchmark Markdown pages rendered from main's READMEs with
77
# those numbers spliced in. Record-only: it never gates a merge
8-
# or a release, and it never pushes to protected main — the
9-
# in-repo committed snapshot stays the `bench-fragments` gate's
10-
# source of truth, hand-refreshed via run.sh. The website pulls
11-
# the assets numbers at build time.
8+
# or a release, and it never pushes to protected main. The
9+
# in-repo committed snapshot — hand-refreshed via run.sh and
10+
# reviewed in a PR — stays the `bench-fragments` gate's source of
11+
# truth AND the numbers the website reads at build time. This
12+
# per-merge run re-measures on a noisy shared runner, so it is a
13+
# record-only drift signal (a demo.gif-style artifact on the
14+
# assets branch), NOT a live source the website or docs read; if
15+
# it fed the published figures they would swing run-to-run with
16+
# whatever else shared the runner.
1217

1318
on:
1419
push:

.github/workflows/pages.yml

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,11 @@ jobs:
8686
# before publishing.
8787
#
8888
# deploy re-runs the same sync and lint after the
89-
# version-stamp + site-assets pulls (which mutate docs/
90-
# before the second sync), so the deploy's second pass
91-
# also covers anything those steps could break — work that
92-
# build-content deliberately skips on PR.
89+
# version-stamp step (which mutates tracked manifests before
90+
# the second sync), so the deploy's second pass also covers
91+
# anything that step could break — work that build-content
92+
# deliberately skips on PR. The demo-GIF pull lands in
93+
# website/static/img/ and never touches docs/.
9394
#
9495
# No Pages permissions — this job never touches the Pages
9596
# API; deploy carries those separately.
@@ -259,25 +260,21 @@ jobs:
259260
# resolved version, so the deployed site shows it.
260261
if: env.SITE_VERSION != ''
261262
run: go run ./cmd/mdsmith-release stamp "$SITE_VERSION"
262-
- name: Pull published demo + benchmark numbers
263-
# The demo GIF and the cross-tool benchmark numbers are
264-
# regenerated post-merge and pushed to the orphan assets
265-
# branch (.github/workflows/{demo,benchmark}.yml), never
266-
# committed. Pull them in so the deployed site serves the
267-
# current GIF as a first-party asset and bakes the live
268-
# benchmark table into the comparison page. The committed
269-
# fragment snapshot is the fallback while the assets
270-
# branch has not published numbers yet; the
271-
# `bench-fragments` gate (a separate workflow) still
272-
# validates that committed snapshot. The scoped
273-
# `mdsmith fix` refreshes only the one page's <?include?>
274-
# body from the freshly pulled fragment — the intended,
275-
# minimal exception to build-website's --no-fix. See
276-
# docs/development/release-tooling.md for why the pull is
277-
# a Go subcommand rather than inline shell.
278-
run: |
279-
go run ./cmd/mdsmith-release pull-site-assets
280-
go run ./cmd/mdsmith fix docs/background/markdown-linters.md
263+
- name: Pull published demo GIF
264+
# The demo GIF is regenerated post-merge and pushed to the
265+
# orphan assets branch (.github/workflows/demo.yml), never
266+
# committed. Pull it in so the deployed site serves the
267+
# current GIF as a first-party asset. The cross-tool
268+
# benchmark numbers are NOT pulled: they come from the
269+
# committed in-repo snapshot under docs/research/benchmarks/
270+
# (refreshed via run.sh, reviewed in a PR), so the noisy
271+
# per-merge benchmark.yml re-measurement stays a record-only
272+
# drift signal and never moves the published figures. The
273+
# `bench-fragments` gate (a separate workflow) validates that
274+
# committed snapshot against the committed JSON. See
275+
# docs/development/release-tooling.md for why the pull is a
276+
# Go subcommand rather than inline shell.
277+
run: go run ./cmd/mdsmith-release pull-site-assets
281278
- name: Build Hugo content tree from docs
282279
# Snapshot ./docs into ./website/content/docs, dropping
283280
# proto.md schema templates, renaming index.md to
@@ -298,10 +295,9 @@ jobs:
298295
# the rationale for each disabled rule. Re-runs here as
299296
# defense-in-depth: build-content already ran the same
300297
# check, but deploy's own sync above happens AFTER the
301-
# version-stamp and `mdsmith fix
302-
# docs/background/markdown-linters.md` steps that mutate
303-
# the docs/ tree, so this pass is the only one that
304-
# covers the post-mutation result.
298+
# version-stamp step that mutates tracked manifests, so
299+
# this pass is the only one that covers the post-stamp
300+
# result.
305301
run: |
306302
go run ./cmd/mdsmith check \
307303
--config ./website/build-output.mdsmith.yml \

.github/workflows/release.yml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,3 +871,131 @@ jobs:
871871
uses: ./.github/workflows/pages.yml
872872
with:
873873
version: ${{ inputs.version }}
874+
875+
# Re-measure the cross-tool benchmark on every release and open a PR
876+
# with the refreshed numbers and the pages that embed them, so the
877+
# committed snapshot the README, the benchmark doc, and the website
878+
# read never freezes between releases (there is no local run.sh step
879+
# in the normal flow). bench-check is the regression gate: it compares
880+
# mdsmith's ratio to mado -- machine- and corpus-size-independent, the
881+
# "same code, same factor" signal -- and fails this job, visibly, on a
882+
# real relative slowdown. It runs AFTER `release` and is intentionally
883+
# not in the publish gating chain: the cross-tool fetch (pinned
884+
# tarballs, npm, Rust clones) has enough external moving parts that a
885+
# flake should not block shipping. The PR lands through the normal
886+
# review-and-merge path; the bench-fragments gate re-checks the
887+
# fragments against the data when the PR is enqueued in the merge
888+
# queue, before it merges. (Like record-secret-rotation.yml, the PR
889+
# is opened with the default token, so ci.yml does not auto-run on
890+
# PR open — the merge queue is where the gate fires.)
891+
benchmark-refresh:
892+
needs: [release]
893+
if: *release_repo_trigger_ok
894+
runs-on: ubuntu-latest
895+
timeout-minutes: 60
896+
# contents: write to push the refresh branch; pull-requests: write
897+
# for `gh pr create`. The change to main still goes through the
898+
# normal review-and-merge path -- this token only opens the PR.
899+
permissions:
900+
contents: write
901+
pull-requests: write
902+
steps:
903+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
904+
with:
905+
# Never persist the token in git config; the push step
906+
# authenticates explicitly via a one-shot, masked header.
907+
persist-credentials: false
908+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
909+
with:
910+
go-version-file: go.mod
911+
cache: false
912+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
913+
with:
914+
# markdownlint-cli2 installs via `npm ci` from the committed
915+
# docs/research/benchmarks/npm/ lockfile.
916+
node-version: "24"
917+
- name: Snapshot the committed baseline
918+
# The committed data/ is the previous release's numbers. Keep a
919+
# copy so bench-check can compare the fresh ratios against it
920+
# after the measurement overwrites data/ in place.
921+
run: |
922+
# rm first so a re-run on a persistent runner replaces the
923+
# baseline rather than nesting it under /tmp/baseline-data/data.
924+
rm -rf /tmp/baseline-data
925+
cp -r docs/research/benchmarks/data /tmp/baseline-data
926+
- name: Re-measure the cross-tool benchmark
927+
run: go run ./cmd/mdsmith-release bench /tmp/mdsmith-bench
928+
- name: Re-splice the fragment includes
929+
run: |
930+
go run ./cmd/mdsmith fix \
931+
docs/research/benchmarks/results.fragment.md \
932+
docs/research/benchmarks/headline.fragment.md
933+
go run ./cmd/mdsmith fix \
934+
README.md docs/research/benchmarks/README.md
935+
- name: Check for regression and open the refresh PR
936+
shell: bash
937+
env:
938+
GH_TOKEN: ${{ github.token }}
939+
run: |
940+
set -euo pipefail
941+
git config user.name "github-actions[bot]"
942+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
943+
944+
# Ratio-based regression gate: mdsmith vs mado within each
945+
# run, so the runner speed and the (growing) corpus cancel and
946+
# only a real relative slowdown trips it. Capture the verdict
947+
# for the PR body and defer failing the job until after the PR
948+
# is opened, so a regressed run still leaves a reviewable diff.
949+
verdict=$(go run ./cmd/mdsmith-release bench-check \
950+
/tmp/baseline-data docs/research/benchmarks/data 2>&1) && rc=0 || rc=$?
951+
echo "$verdict"
952+
953+
git add docs/research/benchmarks/data \
954+
docs/research/benchmarks/results.fragment.md \
955+
docs/research/benchmarks/headline.fragment.md \
956+
docs/research/benchmarks/README.md \
957+
README.md
958+
if git diff --staged --quiet; then
959+
echo "::notice::benchmark numbers unchanged -- no PR opened"
960+
exit "$rc"
961+
fi
962+
963+
branch="bench/refresh-${VERSION}-${GITHUB_RUN_ID}"
964+
git checkout -b "$branch"
965+
git commit -m "bench: refresh cross-tool benchmark numbers for ${VERSION}"
966+
# Authenticate the push via a masked http.extraheader rather
967+
# than embedding the token in the remote URL.
968+
auth_header=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w0)
969+
echo "::add-mask::$auth_header"
970+
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
971+
push origin "HEAD:refs/heads/${branch}"
972+
973+
# Build the PR body with echo appends (no heredoc) so dedent
974+
# and backtick expansion cannot bite. Single quotes keep the
975+
# Markdown backticks literal.
976+
{
977+
echo 'Automated refresh of the committed cross-tool benchmark snapshot,'
978+
echo "re-measured on the CI runner during the ${VERSION} release."
979+
echo ''
980+
echo 'Review the number diff under `docs/research/benchmarks/data/` and the'
981+
echo 'regenerated fragments. Absolute times wobble on a shared runner, so the'
982+
echo '**ratio** is the signal -- the regression gate compares mdsmith to mado,'
983+
echo 'which cancels both the runner speed and the corpus size:'
984+
echo ''
985+
echo '```'
986+
echo "$verdict"
987+
echo '```'
988+
} > /tmp/pr-body.md
989+
gh pr create \
990+
--base main \
991+
--head "$branch" \
992+
--title "bench: refresh cross-tool benchmark numbers for ${VERSION}" \
993+
--body-file /tmp/pr-body.md
994+
995+
if [ "$rc" -ne 0 ]; then
996+
# rc!=0 is either a real regression or bench-check failing to
997+
# read the data (missing tool/file); the verdict echoed above
998+
# says which. Either way fail the job so it is visible.
999+
echo "::error::bench-check failed — perf regression, or it could not read the benchmark data (see the verdict above). The refresh PR opened for review; this run is marked failed."
1000+
exit "$rc"
1001+
fi

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ row: "- [{summary}]({filename})"
9393
- [`mdsmith lsp` emits diagnostics, quick-fixes, and navigation — definition, references, symbol search, and a call-hierarchy over `<?include?>`, `<?catalog?>`, and cross-file links — consumed by any LSP-aware editor.](docs/features/live-diagnostics.md)
9494
- [`mdsmith extract` projects a schema-conformant Markdown file into a JSON, YAML, or msgpack data tree; `mdsmith export` writes a portable, directive-free copy that renders anywhere.](docs/features/markdown-as-data.md)
9595
- [Pin a Markdown convention to get a curated rule preset and a target renderer flavor in one switch. `MDS034` flags syntax the flavor will not render; a placeholder vocabulary spares template tokens.](docs/features/markdown-conventions.md)
96-
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — roughly 4x faster than Node markdownlint, with a CI gate against regression.](docs/features/performance.md)
96+
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — an order of magnitude faster than Node markdownlint, with a CI gate against regression.](docs/features/performance.md)
9797
- [CI badge, Go Report Card grade, and Codecov coverage badge report live project health. mdsmith lints its own docs with the rules it ships, and a coverage gate blocks any merge that drops below the line.](docs/features/quality.md)
9898
- [`mdsmith list query 'status: "✅"' plan/` selects files by a CUE expression on front matter; `mdsmith metrics rank` ranks files by any shared metric — both ready to pipe into a release script.](docs/features/release-gating.md)
9999
- [Rename a heading and every workspace anchor link that points at it is rewritten in one atomic edit. Link-reference labels rename with their uses. A colliding slug fails loudly instead of silently breaking cross-file links.](docs/features/rename.md)

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ row: "- [{summary}]({filename})"
7979
- [`mdsmith lsp` emits diagnostics, quick-fixes, and navigation — definition, references, symbol search, and a call-hierarchy over `<?include?>`, `<?catalog?>`, and cross-file links — consumed by any LSP-aware editor.](docs/features/live-diagnostics.md)
8080
- [`mdsmith extract` projects a schema-conformant Markdown file into a JSON, YAML, or msgpack data tree; `mdsmith export` writes a portable, directive-free copy that renders anywhere.](docs/features/markdown-as-data.md)
8181
- [Pin a Markdown convention to get a curated rule preset and a target renderer flavor in one switch. `MDS034` flags syntax the flavor will not render; a placeholder vocabulary spares template tokens.](docs/features/markdown-conventions.md)
82-
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — roughly 4x faster than Node markdownlint, with a CI gate against regression.](docs/features/performance.md)
82+
- [A single static Go binary, no runtime to start. The workspace walk runs in parallel, embeds are linted once, and `check` is built for the hot path — an order of magnitude faster than Node markdownlint, with a CI gate against regression.](docs/features/performance.md)
8383
- [CI badge, Go Report Card grade, and Codecov coverage badge report live project health. mdsmith lints its own docs with the rules it ships, and a coverage gate blocks any merge that drops below the line.](docs/features/quality.md)
8484
- [`mdsmith list query 'status: "✅"' plan/` selects files by a CUE expression on front matter; `mdsmith metrics rank` ranks files by any shared metric — both ready to pipe into a release script.](docs/features/release-gating.md)
8585
- [Rename a heading and every workspace anchor link that points at it is rewritten in one atomic edit. Link-reference labels rename with their uses. A colliding slug fails loudly instead of silently breaking cross-file links.](docs/features/rename.md)

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ file: docs/research/benchmarks/headline.fragment.md
4545
<!-- Generated by docs/research/benchmarks/gen_fragments.py from
4646
docs/research/benchmarks/data/*.json — do not edit by hand. Re-run
4747
the harness (run.sh) and `mdsmith fix` to refresh. -->
48-
One static Go binary checks 523 Markdown files in about
49-
0.2 s. That is roughly 10x faster than Node markdownlint.
48+
One static Go binary checks 722 Markdown files in about
49+
0.5 s. That is roughly 9x faster than Node markdownlint.
5050
It does more per file than the Rust linters; closing the rest of
5151
that gap is active work.
5252
<?/include?>
@@ -121,8 +121,8 @@ is republished to Open VSX for Cursor, VSCodium, and Theia.
121121
**[Fast on every run](docs/features/performance.md).**
122122
One static Go binary, no runtime to start. The workspace walk runs
123123
across all cores, and includes are linted once. A full check of
124-
this repository's ~720 files takes about 1.3 s, roughly 4x faster
125-
than Node markdownlint.
124+
this repository's Markdown takes about 0.5 s, an order of
125+
magnitude faster than Node markdownlint.
126126

127127
**[Installs everywhere](docs/features/install-everywhere.md).**
128128
The same version-stamped binary ships through go install, npm,

0 commit comments

Comments
 (0)