Changelog¶
Last updated: 2026-09-15
What a project that upgrades will notice, one entry per release. The number lives in
CHECKS-VERSION; the release is the git tag v<number>.
1.4.0 — 2026-09-15¶
A gallery is not a consumer: the scanner refuses a built view nothing mounts.
- The
unreached-viewcheck (rule 08, "Every built surface is on a route"; enforcement task E4.5). A public view or component that nothing on a route in the product constructs refuses the push — on every platform: Swiftpublic struct X: View, a public top-level@Composablescreen, an exported PascalCase component. A debug component gallery, a catalogue, a preview (#Preview,@Preview), a storybook or a test is a construction site and not a route; a feature flag is a route. A view deliberately not mounted yet declares it in the comment above its declaration,not-mounted-yet: <the task that mounts it>, so the deferred list lives with the code. Born of a real miss: a product's home screen built, tested, captured and ticked complete, drawn only by its debug gallery, with every screen that should have sat on top of it falling back to the door. - A new path class,
gallery, ordered aftertestsand beforeui: the catalogue, preview and storybook paths per platform. The string-literal check no longer judges those files (their labels name states, not screens). A project's.coast/paths.jsonmay rebind it like any class. - Rule 08 gains the rule the check holds, and its case log the miss that produced it, with the wording a feature flag needs: a route the product has, on by default or behind a declared flag whose off state is named.
- Rule 06 / TEST-6: every failure path gets a test — one per outcome a model, network or file can return, asserting what the person sees and can do next.
- Rule 00 clause 3, rule 06, TEST-7: done means used — a screen task closes only after the builder drives it in the built product as a person, with captures on disk; the window, title, settings, navigation and relaunch are part of the flow. Both born of the same day's walk of an adopting product (a review that ran and said nothing, a refused key that locked a step).
1.3.0 — 2026-09-09¶
Every platform's push runs on what it changed, not only Swift's.
- Module graphs for Android, the web and Python. The hook reads the platform's own
graph: Gradle modules from
settings.gradle[.kts]and each module'sproject(':x')dependencies (a push builds and tests the affected modules,./gradlew :m:build/:m:test); npm workspaces from the rootpackage.json(a push tests the affected workspaces,npm test -w); and for Python every tracked.pyfile with its imports resolved to files in the tree, thesrclayout and relative imports included, so a push runs exactly the test files whose imports reach what changed, and says so when none do. - The runner's own related-tests mode. When the web test script is
jestorvitest, a push hands the changed files tojest --findRelatedTestsorvitest related: the tests that import the changed files, transitively, whether or not the project has workspaces. - ktlint reads the changed files like the other formatters already did.
tsc,mypy,npm run buildanddetektstill run whole when code changed: the first two are whole-program by nature, the others are not scoped by their tools.
The push battery runs on what the push changed.
- A push of documents alone runs no code check. The hook reads what the push changed
before it runs anything. Documents, plans and other prose (
docs/,plans/,.md,.txtand the like) are not code: the build, the tests, the linter, the formatter, the whole-tree scan, the doc-comment count and jscpd all printSKIPPED — no code changedand the push takes seconds. The scanner still reads the added lines, and the protected-main check still runs. Origin: a push that changed one internal note ran the whole battery, for a change no check could have an opinion on. - A push of code runs the checks on that code. The linter and the formatter read the
changed files. On a Swift package, the build rebuilds the targets that changed and every
target that depends on them, and the tests run for the test targets that depend on them
(
swift test --filter), by the package's own graph: a change in a leaf module runs its tests alone, and the hook says so when no test target depends on what changed. A file no target owns, a change to the checks, a linter configuration, a baseline or the package manifest runs everything, as doesCOAST_SCOPE=allin the environment (CI's word). An Xcode project has no graph the hook reads yet: its build and tests run whole when code changed; its linter and formatter are file-scoped. On the web and Python platforms eslint, prettier and ruff read the changed files; the other tools run whole when code changed. Module graphs for Gradle, npm workspaces and Python packages are filed (E8.6). - The starting lines judge a scoped run by file. A tool's baseline entry
(
build-warnings,lint-findings,format-findings) now carries a count per file beside its total, so a scoped push is held on the files it rebuilt or linted: their count may not rise against those same files' baseline, and a rise names the file. Files the push did not touch keep their numbers. The seats' counters live in one place now (check_rules.py --ratchet <id> --log <log>);--measureprints the per-file lines the installer writes. Upgrading: re-run the installer with--measure-toolsonce, so the baseline gains the per-file counts; until then a scoped push runs those seats whole and prints the line that says so. The re-measure never raises a count. rules/07: "the full gate battery runs before every push" now says what the battery runs on. The rule's check is unchanged.
1.1.1 — 2026-09-08¶
A project that runs Python can complete its first push.
- The linter stops reporting on the layer itself. The seeded
ruff.tomlandmypy.iniread the whole tree, so the checks and the session hook — which ship as they are and are not the project's code — came back as the project's own findings, and a first push could not go green. The seeds now exclude the layer's folder, and the push battery excludes it too, so a seat stays right whatever is later edited into a seed. A Swift or Node project was never affected: the layer holds no source of their kind. Upgrading: re-run the installer. A seed you have edited is left alone, as always — so addextend-exclude = [".coast"]to yourruff.tomlandexclude = ^\.coast/to yourmypy.iniyourself, or the battery will pass while your ownruff check .keeps reporting the layer. - Seeds are rendered, not copied. A linter seed may name a layout key as
{state_dir}the way the settings file and the governing classes already do, so a project that moves the layer's folder gets a seed that matches. The layout table gainsstate_dir_regexbesidestate_dir, for a tool whose exclude is a regular expression. - The module-layering check reads Python.
import-matrix(ARCH-2) knew only the Swift, Gradle and Node layouts, so on a Python project it stopped the push with "unknown platform" — while the Python rules document promised the check. It now reads a Python layout: one package (undersrc/, else the root) makes that package's subpackages the modules and its root files the app target; several packages side by side each count as a module. Asrc.-prefixed absolute import is tolerated, and module names are matched without regard to case, so a lowercasedomainpackage is the shared centre the rule expects.
1.1.0 — 2026-09-08¶
The switches, the config file, and one folder for the layer.
- One folder. The checks and the Claude Code session hook now live under
.coast/(.coast/checks/,.coast/hooks/claude-hook.py) instead ofScripts/. A project that already had ascripts/folder no longer collides with them on a Mac. Re-running the installer moves an existing project over and removes the old copies; theCLAUDE.mdblock,.claude/settings.jsonand the hooks are rewritten to the new paths. - A config file, asked once.
.coast/config.jsonholds the switches and the names. A first install at a terminal asks the on/off questions once — one screen each for the rules, the push-gate seats and the session hooks, every default on;--yesskips the questions,--initasks them again. A switched-off rule is not run, a switched-off seat printsgate: <name> OFF (config), a switched-off session hook exits with a note, a switched-off linter is neither run nor seeded. A severity may be lowered in the config, never raised.retired_wordsadd to the retired-wording check;ratchet_dayssets the starting-line deadline. - The number stays honest. A rule whose every check is switched off is counted as
off, not enforced: "enforced by a check 24 of 74 (3 switched off)" in the verifier, theCLAUDE.mdblock and the session-start line. - Names from the config.
--owner,--productand--orgfillownerin the config; every refusal sentence and theCLAUDE.mdblock read them ("ask Pat Lee in one line first"), with general words when they are blank. No shipped file carries a name. - The block title now reads "Coast Standards — the standing rules, enforced by machines where a machine can hold them".
- Docs. The developer guide gains "Configuration" and the layout table; the options page gains the switches.
1.0.0 — 2026-09-07¶
The first public release, under the Coast Standards License (source-available: use and share unchanged, no resale, no derived products; see LICENSE.md).
- Rules. Six platform rule documents (iOS, macOS, Android, React Native, Web, Python backend) at corpus version 8, plus the AI-features rules; every rule names the check that holds it. Rules enforced by a check: 170 of 643 across the corpus.
- Checks. The rules scanner with per-platform signature tables, the doc-comment check, the import matrix, the type-size advisory, the async-blocking check, the duplicate-code seat (jscpd 5.1.2), the secret scan, and the shipped linter configs.
- Hooks.
pre-commit,commit-msgandpre-pushfor git; the Claude Code session hooks that refuse edits to governed files, infrastructure commands,--no-verifyand force-pushes, and that scan every edited file. - Installer.
adopt.pyinstalls all of it into a project in one command, records starting lines with a 90-day deadline, and upgrades in place. It records the release it installed in.coast/standards-versionand can fetch a named release itself (--release 1.0.0, orlatest), so no clone is needed. - The
CLAUDE.mdblock is written betweencoast-standards: begin/endmarkers. A block written by an earlier private build under the olderup-coast-standardsmarkers is recognised and replaced.