Skip to content

Commit 918bbd9

Browse files
cemililikclaude
andcommitted
fix(tooling): keep prettier off the documentation corpus
Prettier's Markdown printer is not a formatting no-op on hard-wrapped prose. Reproduced against the repo's own prettier 3.3.3: it joins blockquote lines onto the preceding one, drops the `>` from continuation lines, and reads the `*` inside a code span as an emphasis marker — rewriting `/api/v1/*` to `/api/v1/_` and demoting a sentence clause to a list item in phase-01-repository-tooling.md. What fired was the editor. `.vscode/settings.json` maps `[markdown]` to the Prettier extension for the whole repository, so any save or Format Document reaches `docs/`. The hook was never the caller here — hooks are opt-in via `make hooks` and `core.hooksPath` is unset — but it carried the same hazard for anyone who ran it, feeding prettier every staged `*.md` and `*.json` in the tree. Nothing asked for or checked that formatting: no CI job runs prettier, and `make format` invokes it from `frontend/`. So both callers now stop at that boundary. A root `.prettierignore` (`/*` plus `!/frontend/`) covers the editor and any root-cwd run; the hook classifies only paths under `frontend/`, since it runs with `frontend/` as its working directory where the ignore file does not apply. Verified A/B against a deliberately unformatted probe: ignored at the root, still formatted inside the workspace. Restores both damaged documents to main's text and re-applies the intended edits on top, so no prettier residue survives, and records the defect in Phase 01's known-defects list beside the other two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5ac579f commit 918bbd9

5 files changed

Lines changed: 110 additions & 57 deletions

File tree

‎.githooks/pre-commit‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
#
88
# Languages handled:
99
# *.cs → dotnet format (whitespace + style)
10-
# *.{ts,tsx,js,jsx,mjs,cjs} → prettier --write + ESLint via pnpm
11-
# *.{json,md} → prettier --write
10+
# frontend/**.{ts,tsx,js,jsx,mjs,cjs} → prettier --write + ESLint via pnpm
11+
# frontend/**.{json,md} → prettier --write
1212
#
1313
# Intentionally NOT handled: *.yml / *.yaml. Compose / Dapr / APISIX YAMLs
1414
# are comment-heavy and prettier reflows them in a way that hurts review
@@ -80,10 +80,24 @@ all_staged=()
8080
# diff --cached` invocation on every commit).
8181
while IFS= read -r -d '' f; do
8282
all_staged+=("$f")
83+
#
84+
# Prettier is scoped to `frontend/` on purpose. It is a frontend tool here:
85+
# `make format` runs it from that directory, and no CI job verifies its
86+
# output anywhere. Pointed at the documentation corpus it is not a no-op —
87+
# its Markdown printer joins blockquote lines, drops the `>` from
88+
# continuations, and reads the `*` inside a code span as an emphasis
89+
# marker — which silently rewrote `/api/v1/*` to `/api/v1/_` in a roadmap
90+
# record. The root `.prettierignore` draws the same boundary for the
91+
# editor; this filter draws it here, because prettier runs from
92+
# `frontend/` where that file does not apply.
8393
case "$f" in
84-
*.cs) cs_files+=("$f") ;;
85-
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) js_like_files+=("$f") ;;
86-
*.json|*.md) prettier_only_files+=("$f") ;;
94+
*.cs) cs_files+=("$f") ;;
95+
frontend/*)
96+
case "$f" in
97+
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) js_like_files+=("$f") ;;
98+
*.json|*.md) prettier_only_files+=("$f") ;;
99+
esac
100+
;;
87101
esac
88102
done < <(staged_files)
89103

‎.github/CONTRIBUTING.md‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Configure these in **GitHub → Settings → Branches → Branch protection rule
1010

1111
> **Two settings are deferred — maintainer decision, 2026-08-10.** While
1212
> LearnStack has one active contributor, the live `main` rule sets
13-
> **Require approvals: 0** and leaves **Do not allow bypassing** _off_. A second
13+
> **Require approvals: 0** and leaves **Do not allow bypassing** *off*. A second
1414
> approver on a single-contributor repository blocks every merge, and admin
1515
> enforcement with no second admin blocks the only person who could unblock it.
1616
> The settings below are the **target state**, not the current one.
@@ -31,7 +31,6 @@ Configure these in **GitHub → Settings → Branches → Branch protection rule
3131
- Dismiss stale approvals when new commits are pushed: **on**.
3232
- Require review from CODEOWNERS: **off** (no CODEOWNERS file yet).
3333
- **Require status checks to pass before merging**
34-
3534
- Require branches to be up to date before merging: **on**.
3635
- Required status checks (the job names from `.github/workflows/ci.yml`):
3736
- `backend (build + unit + arch + contract)`
@@ -46,7 +45,6 @@ Configure these in **GitHub → Settings → Branches → Branch protection rule
4645
`(deferred …)` suffix, and add the new name both to this list and to the live
4746
branch-protection setting. Setting the variable alone leaves a job that runs
4847
but gates nothing.
49-
5048
- `backend integration (Testcontainers — deferred)` — Phase 02a **Packet 7**,
5149
with the first cross-tenant isolation test.
5250
- `openapi diff (deferred to Phase 02d)` — **Phase 02d**, with the first real
@@ -57,7 +55,6 @@ Configure these in **GitHub → Settings → Branches → Branch protection rule
5755
GitHub matches required checks **by name**, so the rename is the dangerous
5856
half: a renamed check that nobody re-required is a check that no longer blocks
5957
anything, and the PR still shows green.
60-
6158
- **Require conversation resolution before merging**: on.
6259
- **Require signed commits**: optional (off until the team rolls out signing keys).
6360
- **Require linear history**: on (we use squash-merge or rebase-merge, never bubble).
@@ -80,7 +77,7 @@ Per CLAUDE.md § Commit conventions:
8077
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
8178
(replace the model name when authoring with a different assistant).
8279
- `docs(scope)` for doc-only commits; scope ∈ `architecture | decisions |
83-
standards | roadmap` or omitted for cross-cutting changes.
80+
standards | roadmap` or omitted for cross-cutting changes.
8481

8582
## Pull requests
8683

@@ -102,12 +99,23 @@ make test # unit + arch + contract + vitest
10299
```
103100

104101
The pre-commit hook (activated by `make install`) runs, on staged files only:
105-
`dotnet format` on `*.cs`; prettier on JS/TS/JSON/MD; `next lint --fix` on
106-
JS/TS under `frontend/apps/web` — the one workspace with a `lint` script, so
107-
this is exactly what `pnpm -r lint` covers in CI; and, when the binary is on
108-
PATH, `leakwatch scan fs <staged-file>`. So the lint / typecheck / test /
109-
secret-scan pass above is mostly a sanity check. CI re-runs every check as a
110-
hard gate, so a bypassed local commit will fail the PR build.
102+
`dotnet format` on `*.cs`; prettier on JS / TS / JSON / Markdown **under
103+
`frontend/`**; `next lint --fix` on JS / TS under `frontend/apps/web` — the one
104+
workspace with a `lint` script, so this is exactly what `pnpm -r lint` covers in
105+
CI; and, when the binary is on PATH, `leakwatch scan fs <staged-file>`. So the
106+
lint / typecheck / test / secret-scan pass above is mostly a sanity check. CI
107+
re-runs every check as a hard gate, so a bypassed local commit will fail the PR
108+
build.
109+
110+
Prettier stops at the `frontend/` boundary, and the root `.prettierignore`
111+
is what enforces it in your editor — `.vscode/settings.json` maps `[markdown]`
112+
to the Prettier extension, which honours that file, so neither format-on-save
113+
nor an explicit Format Document reaches `docs/`. Keep it that way: prettier's
114+
Markdown printer rewrites hard-wrapped prose — it joins blockquote lines and
115+
reads `*` inside a code span as emphasis — and it corrupted a roadmap record
116+
doing exactly that. Nothing verifies its output outside the frontend workspace
117+
anyway, since no CI job runs prettier and `make format` invokes it from
118+
`frontend/`.
111119

112120
Older Leakwatch builds — including the `v1.5.0` CI pins — only accept a
113121
directory target. The hook detects that and skips the local scan with an

‎.prettierignore‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Prettier is scoped to the frontend workspace.
2+
#
3+
# `make format` already runs it from `frontend/`, and no CI job verifies its
4+
# output anywhere in this repository. Outside that workspace every file is
5+
# hand-authored — the documentation corpus is hard-wrapped prose with
6+
# blockquote-heavy delivery records, and prettier's Markdown printer rewrites
7+
# it: it joins blockquote lines onto the preceding one, drops the `>` from
8+
# continuation lines, and reads the `*` inside a code span as an emphasis
9+
# marker. It silently turned `/api/v1/*` into `/api/v1/_` in
10+
# docs/roadmap/phase-01-repository-tooling.md.
11+
#
12+
# This file is what stops the editor. `.vscode/settings.json` maps `[markdown]`
13+
# to the Prettier extension, which honours `.prettierignore` from the workspace
14+
# root — so ignoring here covers both format-on-save and an explicit
15+
# Format Document. The pre-commit hook runs prettier with `frontend/` as its
16+
# working directory, where this file does not apply, so it carries the same
17+
# boundary as a path filter of its own.
18+
19+
/*
20+
!/frontend/

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ install: .env hooks ## Restore backend NuGet + frontend pnpm deps + activate git
149149
.PHONY: hooks
150150
hooks: ## Activate the repo's pre-commit hook (.githooks/pre-commit).
151151
@git config core.hooksPath .githooks
152-
@printf "$(CYAN)git hooks → .githooks/ (pre-commit: dotnet format + prettier + eslint + leakwatch if available)$(RESET)\n"
152+
@printf "$(CYAN)git hooks → .githooks/ (pre-commit: dotnet format on *.cs, prettier + next lint under frontend/, leakwatch if available)$(RESET)\n"
153153

154154
# ─── Env scaffolding ──────────────────────────────────────────────────────
155155
# `.env` is gitignored; this rule copies `.env.example` on first run so the

0 commit comments

Comments
 (0)