Skip to content

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-commit runs lint-staged, formatting and linting only the files staged for that commit (via prettier --write and ESLint's --fix — through each affected workspace's lint:fix in the RONL Business API, directly in the other two).
  • pre-push first 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 with npm ci named when they differ. Only then does it run the full lint and format-check across the repository (RONL Business API's pre-push also 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 failed check-format on 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-msg rejects 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-push re-checks that pattern across every commit in the push range, so a message that slipped through locally cannot reach the remote.
  • pre-commit regenerates gui/public/changelog.json from the git history via scripts/generate-changelog.mjs, so the in-app changelog cannot drift from what was actually committed. It degrades gracefully when node is 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.