Code Standards¶
Verification status
All three repositories' claims were re-checked on 27 September 2026, against
a7fe76f, 0143ea2 and 3c44b9e: rulesets read from the API per ruleset, workflow
files enumerated and their steps, triggers, filters, runners and pins read from
source, both hook files read from each repository, and the merge-commit settings read
from each repository.
What moved is production. In the Linked Data Explorer and the RONL Business API
a push to main no longer starts the production deploys directly: one promotion
workflow calls them in order, backend first. The rulesets did not move.
This page covers three application repositories — CPSV Editor (ttl-editor), Linked
Data Explorer, and RONL Business API — that share a common tooling convention:
Husky-managed git hooks, npm workspaces, and four identically named root scripts. It
documents what their tooling actually enforces, measured from each repository's own
configuration rather than assumed to be uniform.
The Norm Editor is the fourth application repository and is deliberately out of scope below, because it shares none of that convention — see The Norm Editor is shaped differently at the foot of this page.
Linting and formatting¶
Every repository exposes the same four commands at its root, but the shape behind them differs — RONL Business API and Linked Data Explorer are npm workspaces fanning out across multiple packages; CPSV Editor is a single package.
| Command | RONL Business API | Linked Data Explorer | CPSV Editor |
|---|---|---|---|
| Lint | npm run lint |
npm run lint |
npm run lint |
| Lint, fixing what it can | npm run lint:fix |
npm run lint:fix |
npm run lint:fix |
| Format | npm run format |
npm run format |
npm run format |
| Format, check only | npm run check-format |
npm run check-format |
npm run check-format |
At the root, the four names line up. Underneath, they don't. RONL Business API's root
check-format isn't a workspace fan-out at all — it runs prettier --check directly
against the whole tree (**/*.{ts,tsx,json,md}), so it reaches every package in one
pass regardless of what each package calls its own script. Linked Data Explorer's root
check-format instead runs npm run check-format --workspaces --if-present, which
invokes whichever workspace defines a script of that exact name and silently omits
any that don't, because --if-present treats a missing script as nothing to do rather
than an error.
That distinction was not academic. Linked Data Explorer's backend package used to name
its script format:check, not check-format — a one-character difference from what the
root fan-out was looking for. The root command exited 0 on every run, having quietly
checked only the frontend the whole time. The fix keeps check-format as the backend's
canonical name and format:check as an alias that delegates to it, so the workspace is
picked up under either name. The practical rule this leaves behind: if you're running a
formatter or linter by drilling into a single workspace (npm run check-format
--workspace=@ronl/backend, for example) rather than from the repository root, check
that package's own package.json for the script's actual name — don't assume it matches
the root's.
Git hooks¶
All three repositories wire the same two hooks through Husky, and they gate the same two things everywhere: staged-file linting and formatting on commit, full linting and formatting on push.
pre-commitrunslint-staged, formatting and linting only the files staged for that commit (viaprettier --writeand ESLint's--fix— through each affected workspace'slint:fixin the RONL Business API, directly in the other two).pre-pushfirst checks the installed dependencies against the lockfile —npm run deps:check, the first line of the hook in all three repositories since mid-September 2026 — and stops withnpm cinamed when they differ. Only then does it run the full lint and format-check across the repository (RONL Business API'spre-pushalso rebuilds the shared package and runs a type check first, since its packages depend on it). The dependency check is there because a fast-forward brings lockfile changes in and installs nothing: on 14 September 2026 a RONL Business API clone still had Prettier 3.8.1 installed after the lockfile moved to 3.9.6, and its push failedcheck-formaton seven correctly formatted files, with nothing in the output pointing at the install.
None of the three repositories' git hooks run the test suite. Neither pre-commit
nor pre-push invokes npm test anywhere. A passing hook is not evidence your change
didn't break a test — only CI, or running the suite yourself, tells you that.
And since 19 September 2026 a red test does block a merge to acc, in all three.
The suites had run on every pull request since August 2026, so a failure showed there and
stopped the deploy, but no ruleset named a test workflow as a required check. The build
and deploy checks are now required on acc — and each of those jobs runs the linter, the
type check where one exists and the unit suites before it builds, so requiring the job
requires the suite. On main nothing has changed: a red test still stops the deploy
without blocking the promotion. See What blocks a merge below.
Git hooks silently do not run in a git worktree
All three repositories install Husky, which sets core.hooksPath to .husky/_ —
a relative path, holding generated shims that are not tracked in git. Only
.husky/pre-commit and .husky/pre-push are in the tree.
A git worktree created from any of these repositories therefore has the config
pointing at a .husky/_ directory that does not exist in it. Git does not warn about
a missing hooks path: it finds no hook and runs none. Every commit and every push
from that worktree skips lint-staged, deps:check, lint and check-format
entirely, and the first sign is CI failing on something a hook would have caught
locally — or, worse, nothing failing at all, because the formatting check lives in a
workflow the branch did not touch.
Recorded as ttl-editor#159, and true
in all three. Working in a worktree means running npm run lint,
npm run check-format and npm run deps:check by hand before pushing, or running
npm install in the worktree so prepare regenerates .husky/_. This is also why
the assistant's working rules forbid --no-verify and hook edits outright — a gate
that can vanish this quietly is one nobody should be disarming deliberately as well.
CI¶
Every deploy pipeline that has something to test now runs the suite before it builds, and a failing test blocks the deploy. That has only been true since 20 August 2026 — before then CI and the hooks left the same gap, and RONL Business API's public-site package had the only real test gate anywhere.
RONL Business API — thirteen workflows: an acc/prod pair for each of four
deployable packages, the supply-chain audit, since v2026.09.7 the Semgrep scan, and
since v2026.09.11–12 three more: promote-to-production.yml, which is what a push to
main starts and which calls the four production workflows in order, the daily
dependency-audit and the release sbom:
| Workflow pair | Lint | Type-check | Tests | Notes |
|---|---|---|---|---|
azure-backend-* |
✅ | – | ✅ | Builds, uploads the artifact, and since v2026.09.11 deploys it over OIDC (azure/login then az webapp deploy), then waits until /v1/health reports the deployed commit. The four deploy steps are skipped on a pull request. Also lints the OpenAPI document |
azure-frontend-* |
✅ | – | ✅ | Also runs npm run test:perf, the wall-clock budget, as a step of its own |
azure-publicsite-* |
✅ | ✅ | ✅ | Its build additionally gates on a prerender and a bundle-cleanliness check |
azure-pa-demo-* |
✅ | ✅ | ✅ | The acc workflow also installs Chromium and runs the Playwright E2E suite before the bundle gate; the production one does not |
The frontend's performance budget runs separately because it asserts wall-clock time, which means nothing while more than a hundred test files compete for cores — see The performance budget.
Two changes in v2026.09.6 and v2026.09.7 are worth reading off that table rather than
inferring from it. @ronl/pa-cockpit has no workflow of its own, being a library
rather than a deployable, so its 476 tests ran nowhere in CI; both frontend workflows now
run its suite first, because the frontend imports it. And the backend pair now triggers
on pull_request as well as push
(#87), so its suite — 2008 tests
at the time — runs before the merge rather than after it. That needed no per-step event
guards at the time, unlike the Linked Data Explorer's equivalent change below: the backend
workflow then ended at an uploaded artifact and had no deploy step to gate. Both filters
gained package-lock.json and package.json, because every workspace resolves through
them and a lockfile-only change was built and tested by nothing. The other three
acceptance workflows did not: the changes patterns of the frontend, PA demo and public
site name neither file, so a lockfile-only pull request still skips all three required
site checks, which report success, and only the backend's build runs a suite.
Linked Data Explorer — eleven workflows in the tree: six deployment workflows,
the supply-chain audit gate added in v2026.08.7, the Semgrep scan added in
v2026.09.3, and since v2026.09.7–8 promote-to-production.yml, the daily
dependency-audit and the release sbom (see
Supply-Chain Pinning — the npm tree).
Both backend and both frontend workflows run lint and then the suite before building.
Since v2026.09.5 the backend workflows also lint the OpenAPI description —
npm run lint:openapi, Spectral against the NL API Design Rules 2.2.1 — straight after
ESLint and before the typecheck, so a description that breaks a rule without a
recorded exception stops the deploy. The routes are held to that description by the
test step, which validates every response against it.
The two ropa-site workflows run neither, and correctly so: that package is a static
index.html plus a staticwebapp.config.json, with no build and no test script to run.
Both backends are now deployed by CI. The Linked Data Explorer's end in
azure/webapps-deploy with a publish profile; since v2026.09.11 the RONL Business API's
end in azure/login over OIDC and az webapp deploy — OIDC because SCM basic auth is
disabled on both of its App Services, so a publish profile would be refused. The hand-run
deploy scripts remain there as break-glass only. Since v2026.09.2 the Linked Data
Explorer's backend acceptance workflow also triggers on pull_request, with its five
deploy-side steps gated on the event, so a pull request runs the backend suite before the
merge rather than acc discovering a break afterwards. Both of its backend workflows
then POST a minimal DMN to the deployed /v1/dmns/validate and fail on a
NODE_MODULE_VERSION mismatch — the one failure a runner-side require cannot see.
Neither production backend workflow has a pull_request trigger, and since 24 September
2026 for the same reason in both: main is promoted from acc, whose pull requests
already ran the suite. The Linked Data Explorer used to give a second reason — that its
production environment required a reviewer, which would have put a human approval in
front of the tests — and that reviewer was removed on 24 September
(#210).
Since v2026.09.1 all four of its deployment workflows also run a Typecheck step.
That closed a real hole rather than adding ceremony: fifteen type errors had
accumulated invisibly, because nothing in the repository ran tsc at all. build is
vite build, which strips types through esbuild without checking them; lint is
ESLint; test is Vitest. None of the three typechecks, so a type error could reach
acc and deploy. A typecheck script now exists at the root and in both workspaces.
In the Linked Data Explorer (v2026.09.7) and the RONL Business API (v2026.09.11) a push
to main no longer deploys anything directly. promote-to-production.yml starts on
every push to main — deliberately with no path filter, because it is what decides which
deploys are needed — and calls the production workflows as reusable workflows: a
changes job first, then the backend, then the sites together, each site running only
when the backend's result is success or skipped. The production workflows kept
workflow_dispatch and lost push; their path rules moved into
scripts/promotion-targets.mjs and scripts/promotion-targets.sh, which fail safe to
deploying everything when the commit range cannot be read. The Linked Data Explorer's
changes job runs the script's own test first — node scripts/promotion-targets.test.mjs
— so a promotion carries one suite of its own. Two consequences worth knowing: the
deploys are jobs inside the promotion's run, so an Actions listing shows Promote to
Production and not the deploys; and if the promotion workflow breaks, nothing deploys,
and nothing on main says so, because its checks are not required. The CPSV Editor is
unchanged: Deploy PROD (white-sky) still triggers itself on a push to main.
CPSV Editor — seven workflows: two Azure Static Web Apps workflows, acc and main
alike, running npm ci, npm run lint and npm run test:ci ahead of the deploy action
— and, since 19 September 2026, a Build step of their own that runs npm run build on
the runner and asserts dist/index.html exists before the deploy action uploads it with
skip_app_build: true — plus the supply-chain audit, since v2026.09.3 the Semgrep
scan (see
Supply-Chain Pinning — the npm tree), and since v2026.09.6
close-preview-environments.yml, and since v2026.09.7 the daily dependency-audit and
the release sbom. The preview workflow holds the two jobs that delete a pull
request's Static Web Apps preview when it closes. They used to sit in the deploy
workflows, whose paths-ignore applies to the close event too — so a documentation-only
pull request never started the workflow holding its close job, and its preview kept
running on a public URL. The new workflow has no path filter at all. Since v2026.09.2 that audit job also runs
npm run check-format and npm run check-supply-chain — and it gained its first
npm ci to do so, everything in it having previously run from npx or plain node.
Since 25 September 2026 a Lockfile matches package.json step runs before that
install in all three repositories — npm ci --dry-run --ignore-scripts — so a lockfile
out of step with its manifest fails under its own name rather than as an EUSAGE three
steps into a job about pinning. It checks the pull request's own merge commit, and no
more: two dependency pull requests each green against an older base can still merge into
a lockfile neither was tested against, because no ruleset requires a branch to be up to
date. That happened in the RONL Business API on 25 September (#221–#223); merge
dependency pull requests one at a time, rebased. Since v2026.09.0 the deploy workflows are named
Deploy ACC (orange-beach) and Deploy PROD (white-sky); both were previously
called Azure Static Web Apps CI/CD, with both jobs named Build and Deploy Job, so a
production run was indistinguishable from an acceptance one in the Actions list, in a
pull request's checks, and in gh run list — telling them apart meant opening the run
and reading which API token it used. Renaming a job renames the check it reports, which
is why it was done before any deploy check is made required.
Those two workflows also skip documentation-only changes, via paths-ignore on
docs/**, .claude/** and **/*.md — on their push trigger. Since 19 September 2026
Deploy ACC (orange-beach) carries no path filter on pull_request: its check is
required on acc, and a workflow filtered out at its trigger reports nothing, so the
same pattern moved into a changes job that skips the build instead. Deploy PROD
(white-sky) keeps the filter on both triggers, its check not being required. See
how a path-filtered workflow became requireable.
The direction of the filter itself is deliberate: an allowlist
(paths:) would mean enumerating every path that affects the build, and anything
forgotten from such a list silently skips a deploy — a worse failure than one
unnecessary preview. RONL Business API and Linked Data Explorer can use paths:
because packages/frontend/** is a real boundary there; the CPSV Editor is a single
package with no such boundary, so copying that pattern would be the obvious move and
the wrong one.
An allowlist has to name the files every package shares, too. Until v2026.09.4 the
Linked Data Explorer's filters named each workflow's own package and nothing else, so a
change to the root package-lock.json alone — Renovate's lock-file maintenance, above
all — was built, tested and deployed by nothing. The root package.json and
package-lock.json are now in both backend and frontend acceptance filters and, for
production, in the promotion's target rules.
A workflow's own file in its paths: list is a trigger. Each Linked Data Explorer
filter names its workflow file alongside its package, which is correct — a change to how
a thing deploys should redeploy it — but it means a sweep across workflow files, such as
a pinning pass, redeploys every package whose workflow it touched, whether or not the
package changed. Predict what a merge will deploy from the whole list, not the entry that
looks like the package.
And .nvmrc belongs in every one of them. Since 19 September 2026 each repository
names its Node version once, in .nvmrc, read through node-version-file. A filter that
does not list that file means a Node bump builds and deploys nothing — the version
moves and no artifact does. Every Node-building deploy workflow's allowlist in the Linked
Data Explorer and the RONL Business API now names it — push filter, changes pattern
and, for production, the promotion's target rules. The ROPA site does not, correctly: it
is static files with no Node step. The CPSV Editor filters with a denylist, which never
excluded .nvmrc. The sharper half is what the build checks being required added: a pull
request changing .nvmrc alone showed every required build check skipped, and was
therefore mergeable having tested nothing.
A skip marker in a commit message turns every gate off
GitHub Actions skips all push and pull_request workflows for a commit whose
message contains [skip ci], [ci skip], [no ci], [skip actions] or
[actions skip] — anywhere in the message, including in prose that is only
discussing them. A skipped required check reports nothing rather than failing,
so the symptom is a pull request that can never become mergeable and has no red
run to explain why. Describe the markers in words in a commit message; they are
safe in a file. All three repositories compose merge commits from the pull
request title with a blank body, so a marker quoted in a pull request
description cannot reach its merge commit — check that repository setting before
relying on it anywhere else.
None of this replaces running the suite yourself before opening a merge request — and a green local run is weaker evidence than it looks. RONL Business API's first gated run failed on test files that had been latently broken for weeks: ts-jest caches type diagnostics per file, so a warm local cache kept skipping the check that CI, starting cold, performed immediately. Clearing the cache reproduced it at once.
What blocks a merge¶
Running a check and being able to block on it are different things. Which checks each
branch requires, what the rulesets carry beyond them, and how merge method is
enforced are on Branch Protection. The short version, because it
is the fact most often got wrong — and because it reversed on 19 September 2026: on
acc, every repository now requires audit, scan and its build and deploy checks, so
a red test blocks the merge. On main no repository requires a build or a test, and the
CPSV Editor's main requires no status check at all.
Supply-chain hardening¶
Alongside the test gate, all three application repositories — CPSV Editor (the pilot,
v2026.08.2), RONL Business API, and Linked Data Explorer (v2026.08.7) — have
adopted a common set of pipeline controls: every uses: reference pinned to a commit
digest rather than a tag, permissions: contents: read as the workflow default,
persist-credentials: false on checkout, a blocking zizmor audit job, and Renovate
maintaining the digests under a 14-day cooldown with a no-cooldown lane for security
advisories. Since 19 September 2026 three more joined the set: every job pinned to
ubuntu-24.04 rather than ubuntu-latest, one exact .nvmrc per repository read
by every deploy workflow, and a root .npmrc setting min-release-age=14, the
package-manager half of the cooldown Renovate already applies to its own proposals. What
cannot be pinned is written down in each repository's SECURITY-PIPELINE.md rather
than glossed over.
Two Renovate settings joined that list in September 2026, and they matter more than they
look. Lock-file maintenance is what moves the transitive tree at all — Renovate
maintains the dependencies you name, not what they resolve to, and config:recommended
leaves it disabled; all three repositories now run it on a weekly schedule. And majors
sit behind Dependency Dashboard approval rather than a global approval flag, so the
concurrent-pull-request limit is spent on the updates that can actually merge. See
Supply-Chain Pinning — Renovate maintains dependencies, not the tree.
Since v2026.09.0 in the CPSV Editor, and since 29 August 2026 in the other two, the
audit job also validates renovate.json with --strict — pinning without working
automated updates decays into an unpatched tree, so a Renovate that has silently stopped
running is itself a supply-chain failure. That is not hypothetical: five keys used as JSON comments in the Linked Data Explorer's
configuration were rejected as invalid and Renovate stopped opening pull requests, with
nothing in CI noticing.
The rollout is not identical across the three. All three are now on the v7 action
majors — RONL Business API first, the CPSV Editor since v2026.09.0, and the Linked Data
Explorer since v2026.09.2, which retired its last actions/checkout at v3.7.0 on the
way. The CPSV Editor is now the only one whose main is ungated, by decision; the
Linked Data Explorer and the RONL Business API both hold a promotion pull request to a
ruleset of its own — a pull request plus audit, and scan in the Linked Data Explorer —
which is fewer checks than acc requires, and no build.
Supply-Chain Pinning covers the mechanism, what the gate deliberately does not protect, and the order to copy the four artifacts into the next repository. The controls index is the one-page version of where each control holds.
Commit messages¶
Follow Conventional Commits, as described in Contributing → Commit your changes. None of the three repositories above enforce this mechanically — the Norm Editor does, and how it does so is covered in The Norm Editor is shaped differently.
No Claude attribution trailers. If you're using an AI assistant to help prepare a
commit, the commit message ends with its substantive body and nothing else —
Co-Authored-By and similar trailers are not added, regardless of what a tool's default
template suggests appending. This applies whether the change came from Claude Code, the
superpowers plugin, or any other assistant.
Testing¶
Each application repository documents its own testing setup, and this page doesn't repeat it, because counts and commands there go stale the moment a suite grows:
New code is expected to arrive with tests written red/green — the failing test first, watched to fail for the right reason, then the minimum code to pass. For the mechanics of that cycle, see Working with Claude Code rather than this page.
The 80% branch floor, natively enforced¶
All three repositories hold new and changed code to 80% branch coverage per file, and as of September 2026 all three enforce it natively — in their test runners' own configuration, with no exemptions and no custom script:
| Repository | Where |
|---|---|
| CPSV Editor | vite.config.mjs — { branches: 80, perFile: true } |
| Linked Data Explorer | packages/backend/jest.config.js and packages/frontend/vite.config.ts |
| RONL Business API | All five workspace runner configs |
This page said the opposite until 9 September 2026, and was right at the time
Through v2026.09.1 the floor was a convention held by review: no threshold was configured anywhere and no workflow measured coverage. Two things changed it — RONL Business API and the Linked Data Explorer configured native thresholds, and the CPSV Editor retired the ratchet script that had been carrying its last exemption. A claim about enforcement is exactly the kind that goes stale without a word of its own text changing.
The route each took differs, and the CPSV Editor's is the instructive one: Vitest's threshold globs are additive rather than overriding, so a repository not yet at 80% everywhere cannot express a partial rollout and needs a ratchet script instead. The Coverage Floor covers that mechanism, why branches rather than functions, the load cost of bringing a large file up, and how to prove a threshold actually bites rather than trusting a green run.
A floor only gates where the tests run before the merge, and as of 12 September 2026
all three do. The RONL Business API's backend workflow triggered on push alone until
v2026.09.7, so its suite — 2008 tests at the time — ran only after a merge and its
branch threshold gated nothing on a pull request; the trigger closed that
(#87) and proved itself
immediately, when an axios 1.18 security bump broke the backend build on the pull
request rather than on acc.
@ronl/shared is deliberately outside this: it has no test script and needs none,
being types plus constant data with no functions and no branches. That exemption has
a consequence worth knowing — executable logic placed in the shared package is
unmeasurable by construction, which is why a branching helper was moved out of it in
v2026.09.4 rather than left where a passing test run concealed the gap. That move was
caught by hand, by someone who happened to look, and v2026.09.7 replaced the
convention with a check
(#84): check-shared runs in
the audit job and fails on a function, a class, a conditional or a loop anywhere in the
package. It uses the TypeScript compiler API rather than a pattern over text, because an
arrow in an interface is a type and the same syntax assigned to a const is logic, and no
regex separates them.
The Norm Editor is shaped differently¶
The three repositories above converged on one toolchain. The Norm Editor did not, and the differences are structural rather than stylistic — which is why applying this page's expectations to it would produce wrong answers rather than merely strict ones.
| The other three | Norm Editor | |
|---|---|---|
| CI | GitHub Actions | GitLab CI (.gitlab-ci.yml) |
| Languages | TypeScript / JavaScript | JavaScript and Python, five services |
| Repository shape | npm workspaces (two of three) | No root package.json at all — each service stands alone |
| Git hooks | Husky + lint-staged |
.githooks/, plain shell, no Husky |
| Deploy artifact | Static Web Apps bundle or artifact | Docker images pushed to Azure Container Registry |
Its hooks enforce more, not less¶
The other three gate formatting and linting on commit. The Norm Editor's hooks do something the others do not attempt:
commit-msgrejects any subject line not matching the Conventional Commits pattern, with merge, revert and fixup commits explicitly exempted. A non-conforming message hard-fails rather than merely failing review.pre-pushre-checks that pattern across every commit in the push range, so a message that slipped through locally cannot reach the remote.pre-commitregeneratesgui/public/changelog.jsonfrom the git history viascripts/generate-changelog.mjs, so the in-app changelog cannot drift from what was actually committed. It degrades gracefully whennodeis absent rather than blocking the commit.
That last one is the reason its changelog is git-log-derived rather than hand-curated, and why its entries read as commit subjects. A sync reading it should expect terse text and check the source for anything substantive.
What it does not have¶
No lint or format script is wired into CI, and no coverage is measured anywhere — neither runner is invoked with coverage flags and no threshold is configured. Its pipeline gates on tests alone. See Testing for what those tests are and what the pipeline actually blocks.