Status: Accepted Date: 2026-04-21 Authors: Architect agent, with maintainer review
The persona Pages MVP (PR #223) added a new build surface at /site/ — browser-side ES modules, CSS, HTML, and a Python builder (scripts/build_persona_site_data.py) that emits JSON consumed by that renderer. The surface is governed at a module-boundary level by ADR-010 (location) and at a data-contract level by ADR-011 (schema). The PR #223 review surfaced two pre-commit coverage gaps in §8:
- [REC-07] — a YAML or builder edit that broke the persona-site pipeline was only caught in CI.
BLOCK-02in the same review would have been caught locally if a pre-commit gate had re-run the builder against staged inputs. - [REC-08] —
.mjs,.css,.html, and.mdundersite/**had no local formatter. Prettier was configured but scoped torisk-map/yaml/**, so stylistic drift on the new surface would accumulate commit-by-commit.
ADR-005 adopted pre-commit as the single orchestration layer for repository-wide git hooks, and its Follow-up (line 96) left one open question:
If the repository adds a non-Python orchestration surface in the future (for example, a site build under
risk-map/site/), confirm thatpre-commitremains the right orchestration layer for that surface or document the split explicitly.
ADR-010 superseded the path in that follow-up (risk-map/site/ → /site/) but explicitly left the orchestration question open. This ADR resolves it: pre-commit remains the right layer for the new surface, extended via two new local hooks.
The primary evidence commit is 93cc22b — "feat(hooks): add persona-site builder and site-assets prettier hooks" — which adds both hook entries to .pre-commit-config.yaml, both wrapper modules under scripts/hooks/precommit/, and their tests under scripts/hooks/tests/.
Keep pre-commit as the sole orchestration layer and extend it with two new local hooks for site/**. The hooks land under the existing - repo: local block in .pre-commit-config.yaml alongside prettier-yaml.
validate-persona-site-build — wrapper scripts/hooks/precommit/validate_persona_site_build.py. Runs the persona-site builder end-to-end (load_yaml → build_site_data → write_site_data) into a tempfile.TemporaryDirectory, catching schema-validation failures and builder regressions at commit time rather than at CI time. Configuration:
language: system,entry: python3 scripts/hooks/precommit/validate_persona_site_build.py.pass_filenames: false— the wrapper does not operate per-file; it rebuilds the whole pipeline once regardless of which trigger fired.argvis discarded by contract.files:matchesrisk-map/yaml/(personas|risks|controls).yaml,risk-map/schemas/(risks|persona-site-data).schema.json, andscripts/build_persona_site_data.py— every input the builder reads and the code that reads them.- Failure surfaces a single stderr line of the form
Persona-site builder failed: <ExceptionType>: <message>and exits1.
prettier-site-assets — wrapper scripts/hooks/precommit/prettier_site_assets.py. Auto-format ("Pattern A") over site/** frontend assets, mechanically identical to prettier-yaml. Configuration:
language: system,entry: python3 scripts/hooks/precommit/prettier_site_assets.py.files: ^site/.*\.(mjs|css|html|md)$,pass_filenames: true.require_serial: true— matches theregenerate-*generators in the same config. The wrapper runsnpx prettier --write <path>followed bygit add <path>per file, and parallel batches would race over.git/index.lock(the same mechanism ADR-005 calls out for auto-staging hooks).- Style reconciliation lives in
.prettierrc.yml: the repo-wide defaults aresingleQuote: true, semi: false, but two per-file overrides setsite/**/*.{mjs,js}tosingleQuote: false, semi: true, trailingComma: 'all'(standard JS convention) and disableembeddedLanguageFormattingonsite/**/*.mdso documented code samples stay faithful to the real files. The overrides were landed in the same commit range as the hook.
The files themselves were normalized during the MVP's BLOCK-05 work, so the first run of prettier-site-assets on the branch produced no churn.
- CI-only validation. Rejected on the same grounds ADR-005 rejected "no hooks at all": schema and builder errors are cheap to catch locally (the builder runs sub-second) and expensive to catch at PR time after push and reviewer assignment. Pre-commit is the cheap local gate; CI remains the authoritative final gate.
- Ignore site-asset formatting. Rejected per [REC-08]. The MVP added ~1,020 lines of
.mjs, ~567 lines of CSS, and ~35 lines of HTML in a single PR; stylistic drift on that volume would accumulate commit-by-commit without a local formatter, and deferring it to manual review is a standing tax on every futuresite/**edit. - Extend
prettier-yamlto cover site assets. Rejected. The two hooks operate on different file types with different style rules — YAML at the repo-wide defaults,.mjsunder the per-filesingleQuote: false, semi: trueoverride. Overloading one hook would either force a single regex to match both shapes or force both file types to share a style configuration they do not share. Splitting the hooks matches the.prettierrc.ymlsplit. - Framework-packaged
prettierhook (e.g.,mirrors-prettier,rbubley/mirrors-prettier). Rejected for the same reason ADR-005 rejected it forprettier-yaml:language: systemwithnpx prettierreuses the project's already-pinned Node environment and.prettierrc.yml, avoiding a second prettier version to track and avoiding a newadditional_dependenciessurface. - Framework-installed Python environment (
language: python+additional_dependencies) forvalidate-persona-site-build. The PR #223 implementation plan initially proposed pinningPyYAML==6.0.3andjsonschema==4.26.0underadditional_dependencieswithlanguage: python, which would have created an isolated pre-commit venv for the hook. Rejected in favor oflanguage: systembecauserequirements.txtalready pinsjsonschema==4.26.0as part of ADR-011's contract, andvalidate_persona_site_buildimportsscripts.build_persona_site_datadirectly — a separate framework-installed venv would shadow the project venv and could drift fromrequirements.txt.language: systemreuses the single source of truth and preserves ADR-005's "local hooks run in the project's own Python environment" boundary. The material cost of this choice is asys.pathsplice in the wrapper (see Consequences). - Build into
site/generated/in the working tree and diff it back. Rejected forvalidate-persona-site-build. A working-tree build would either leak generated JSON into the repo (fightingsite/generated/'s gitignored status) or require teardown logic that races with parallel framework invocations.tempfile.TemporaryDirectory()isolates the build fully: the hook writes to<tmp>/site/<output>via the builder's ownresolve_output_path, the directory is cleaned up by the context manager, and a test (test_validate_persona_site_build.py) asserts the reposite/generated/stays untouched.
Positive
- Builder drift is caught locally. Any edit to
personas.yaml,risks.yaml,controls.yaml, the risks or persona-site-data schema, or the builder itself re-runs the full pipeline before the commit lands. BLOCK-02-class defects (stringified nested lists silently flowing into the DOM) are now a pre-commit failure, not a CI failure. - Site asset formatting is consistent by construction. Auto-format on every commit means no reviewer spends cycles on prettier diffs, and the
.mjsconvention (double quotes, semicolons, trailing commas) stays stable under edits by contributors who default to the repo-wide YAML style. - ADR-005's open question is closed.
pre-commitcovers the new non-Python surface vialanguage: systemlocal hooks. No split orchestration layer is needed; the existing framework extends cleanly. - Consistent with prior patterns.
require_serial: trueonprettier-site-assetsmatches the generators in the same config and the mechanism ADR-005 documents; the wrapper's body is structurally identical toprettier_yaml.py;TemporaryDirectoryisolation mirrors what any sandboxed build would do.
Negative
- Two more hooks for contributors to understand. The
.pre-commit-config.yamlalready encodedpass_filenames,require_serial, and regex-filter semantics; ADR-005 flagged this learning curve. The new hooks add nothing novel but extend the surface area by two entries. validate-persona-site-buildimports the builder module directly.scripts.build_persona_site_datais now load-bearing for pre-commit: a refactor that changesload_yaml,build_site_data,write_site_data,resolve_output_path, or theDEFAULT_*_PATHmodule constants will break the hook. The hook's tests assert the contract, but the coupling is real.language: systemrequires asys.pathsplice in the wrapper.scripts/hooks/precommit/validate_persona_site_build.pylines 15–18 insert the repo root ontosys.pathbefore importingscripts.build_persona_site_data, becauselanguage: systemdoes not installscripts/as a proper package. The splice is the material cost of the venv-reuse choice above; a future move tolanguage: pythonwould need to drop it in favor of a properly installed package (and re-accept the venv-drift risk the current design avoided).npx prettierinvocation cost onsite/**edits. Each staged file spawns annpxprocess. For the current MVP-sized surface this is negligible; ifsite/**grows materially, a batched invocation (onenpx prettier --writecall with all paths) would be cheaper. The wrapper's current shape matchesprettier_yaml.py; if one is batched later, the other should follow for consistency.first-failure-winsexit semantics are soft contract. Both wrappers preserve the earliest non-zero returncode and continue through remaining files; a contributor reading only the final stderr may miss later failures.prettier_yaml.pyhas the same property, so the behavior is consistent, but it is not loud.
Follow-up
- Closes ADR-005 line 96. The substantive question — is
pre-committhe right orchestration layer for a non-Python site build — is resolved in the affirmative. If a future consumer surface proposes a radically different toolchain (for example, a Vite or esbuild pipeline that wants its own hook runner), revisit this ADR explicitly rather than extending local hooks unbounded. - Cross-refs to peer ADRs.
validate-persona-site-buildenforces the contract ADR-011 defines; itsfiles:regex names the schemas ADR-011 authors. Thesite/**globs it uses depend on ADR-010's module boundary. If ADR-011's schema is renamed, update this hook'sfiles:regex in the same change. - Test coverage on the hook tests.
test_validate_persona_site_build.pyandtest_prettier_site_assets.pywere authored TDD-first per the MVP implementation plan (phase 6). If either wrapper's behavior changes (for example, a move to batchednpxinvocation), those tests are the contract to update first.