← The specification

Tooling

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted

The external tools the project relies on. Every one is free for public repositories or fully open source — no paid, team, or enterprise tier is required, because none is available to an open project on a free org.

Satisfies: ci-cd, security, roadmap M10.


The constraint that shapes every choice

The org is on GitHub’s free plan. That rules out org-level rulesets, required workflows, and other Enterprise features — so branch protection is configured per repo, and anything org-wide is achieved through mechanisms free plans do have: the .github repo for inherited community files, reusable workflows sourced from the spec, and shared config presets.

Every tool below was chosen to work within that constraint.

The toolchain

Concern Tool Free basis Where
Governance gate spec-check (in-repo) our own all repos
Spec integrity integrity.py (in-repo) our own spec
Code quality + coverage SonarQube Cloud free for public all repos
SAST CodeQL free for public each repo shared/adoption.toml records as carrying the alert gate (#358)
Secret scanning gitleaks OSS all repos
Dependency/vuln scanning OSV-Scanner OSS all repos, via the shared security.yml
Supply-chain posture OpenSSF Scorecard free for public each repo that publishes an artefact or the specification; the newest four are outstanding (Q-R59)
Rust licences + advisories cargo-deny OSS lemonfiber
Coverage generation cargo-llvm-cov OSS lemonfiber → Sonar
Workflow lint actionlint OSS all repos
Spell check typos OSS all repos
Link check lychee OSS all repos
Markdown lint markdownlint OSS all repos
Dependency updates Dependabot native to GitHub all repos
Pre-commit hooks none — git’s own — .githooks/, turned on per clone; see below (OPS-R51)
Pre-push guard .githooks/pre-push our own each repo shared/adoption.toml records as carrying it, via core.hooksPath
Task runner just OSS the Rust, spec, stack, brand and site repos; the npm and Composer repos use their own script runner
Changelog git-cliff OSS lemonfiber, sdk-ts, sdk-php
Release binaries cargo-dist OSS lemonfiber
Docs site Astro Starlight OSS website-docs.lemonfiber.app
Web lint ESLint (typescript-eslint) OSS lemonfiber-web, sdk-ts
Web format Prettier OSS lemonfiber-web, sdk-ts
Web accessibility axe-core, driven by Playwright OSS lemonfiber-web, website-docs.lemonfiber.app

Why these, specifically

SonarQube Cloud carries quality and coverage

Free for public repos, native GitHub Action, analyses Rust. It ingests the lcov cargo-llvm-cov produces, so coverage lives there too — no separate Codecov. Per Q-R61, coverage on applicable code is a merge gate: cargo-llvm-cov enforces 100% over that set locally and in CI, and Sonar surfaces the same number. The scope — not a lax gate — is what keeps it honest; the excluded categories are annotated in the source (Q-R62).

The free plan’s own quality gate cannot be raised to this standard, so both the coverage gate and a zero-open-issues gate are enforced in CI directly rather than by Sonar’s gate (ci-cd, Q-R64).

Requires a one-time SONAR_TOKEN secret, added to the repo after it exists.

Dependabot, and the gate that cites for it

Every dependency bump must cite GOV-R12 (canonical-spec). Dependabot cannot write a trailer: commit-message sets a prefix and a scope, and there is no body, no template and no way to add a line to the pull request. So spec_check.py supplies GOV-R12 for a pull request GitHub says dependabot[bot] opened, and then runs the existence check unchanged — the identifier still has to exist on spec@main, and the run still prints what it accepted. The exemption reads pull_request.user.login and nothing the pull request carries; see ADR-0016.

dco needed no change — dco_check.py already exempts a bot-authored commit, because GitHub authors it and there is no human to attest.

Each repo carries its own .github/dependabot.yml. There is no org-level preset: unlike Renovate, Dependabot cannot extend a shared config, so the policy lives in one copy per repo and the copies are kept alike by review.

The policy those copies carry: minor and patch bumps travel as one grouped pull request per ecosystem, a major is left out of the group so it arrives on its own carrying the major label, and the lemonfiber/spec shared-workflow pins are their own group, excluded from the three-day release-age wait — a fixed gate reaches its consumers without one. Security updates are exempt from the wait by construction and are never grouped, so each vulnerability arrives as its own pull request.

What Dependabot does not watch

Renovate was configured to watch four things by regular expression and three more by mechanisms Dependabot has no equivalent of. Renovate never ran, so nothing here regressed in practice — but the intent was real. The two scanner versions are now watched again, by the two different means below; the rest is not.

What Where it lives now Who notices it going stale To bring it back
gitleaks CLI version .github/workflows/security.yml — GITLEAKS_VERSION, read both by the step that downloads it and by the step that checks it The step that checks it, which fails the run once the release it names has been superseded for more than 30 days Done, but not by Dependabot — see below
osv-scanner version .github/workflows/security.yml — the google/osv-scanner-action/osv-scanner-action pin Dependabot, under github-actions Done. The action is a container action, so the uses: pin is what fixes the scanner version
python-version: written inline in workflows nine sites, all "3.12" — seven here, one in lemonfiber, one in brand Nobody, until 3.12 leaves support in Oct 2028 No Dependabot manager reads it; a scheduled check, or .python-version + a linter that reads it
node-version: written inline in workflows nowhere — every repo uses node-version-file: .nvmrc n/a The manager watched nothing; it can simply go
.nvmrc four repos — lemonfiber-web, sdk-ts, website-docs.lemonfiber.app, website-lemonfiber.app Nobody. engines: node >=26 in package.json is a floor, not a bump No Dependabot manager reads .nvmrc
Lockfile maintenance (a weekly refresh with no manifest change) n/a Nobody. Transitive dependencies drift until a direct bump moves them No equivalent; npm update / cargo update on a schedule
Semver ranges pinned to exact versions (:pinAllExceptPeerDependencies) n/a n/a No equivalent. Dependabot updates within a range and widens it; it does not pin
The dependency dashboard n/a n/a No equivalent. The closest thing is each repo’s Dependabot alerts tab

Two of the three that matter are now closed. The third, python-version:, is not, and stays as written above.

The two scanners, and why they are watched differently

Both tools publish a GitHub Action, and calling one turns an unwatched string in a run: block into a pinned uses: that Dependabot already tracks under ADR-0009. That worked for one of them.

osv-scanner is called as google/osv-scanner-action/osv-scanner-action, pinned by SHA. It is a container action, so the pin selects the scanner image as well as the action, and Dependabot advances both together. The hand-written exit-code arm that treated 128 as “no package sources, nothing to scan” is now the scanner’s own --allow-no-lockfiles flag; the action’s entrypoint maps 128 to success either way, and warns when the flag is absent.

gitleaks cannot be. gitleaks/gitleaks-action requires a GITLEAKS_LICENSE secret for any repository an organisation owns — still true at v3.0.0, and this org owns every repository here. The key is free to obtain, but it is a registration, a secret each caller of the shared workflow would have to forward, and a licence check against a third-party server standing on the critical path of a required gate. The action is also not open source: since v2.0.0 it ships under a Gitleaks LLC end-user licence agreement, where the CLI stays MIT. gitleaks/gitleaks itself publishes no action, and Dependabot’s github-actions parser skips a uses: docker://… reference, so the published container is no route either.

So the CLI stays fetched by hand, and a step beside the scan reads the latest gitleaks release and compares it to the version that just scanned. Being behind is a notice for the first month and a failure after it — the same split, for the same reason, as pins in hygiene.yml. An answer that cannot be read after three attempts is a failure, not a pass: a watch that says nothing when it could not look is the failure it exists to prevent.

The hooks, and why there is no hook manager

OPS-R51 asks for a pre-commit hook running the fast CI-blocking checks. lefthook was chosen for it once — a single Go binary, no runtime to install — and six repos carried a lefthook.yml written to that brief. None ever installed it. No recipe or package script ran lefthook install, so the configs described hooks that had never run, and one of them named a scripts/guards.mjs that had been renamed underneath it.

They could not have been installed alongside the other local hook anyway. The pre-push guard needs core.hooksPath .githooks, and git then reads that directory only — anything a manager wrote to .git/hooks is ignored. lefthook 2.x detects the setting, refuses to install, and offers --reset-hooks-path, which turns the guard off. The one command that repairs the dead config disables the working one.

So the hooks are files in .githooks/, and there is no manager:

file what it does shared?
pre-push refuses a push that would leave a branch carrying no commit origin/main does not — the shape of a mistake that destroyed two branches and closed their pull requests in one day — and a push straight to main yes, byte-identical
commit-msg says now what commitlint, dco and spec-check would say after a push: a conventional subject, a sign-off, a Spec: citation yes, byte-identical
pre-commit the fast checks that repository runs — formatting, typos, lint on staged files no, per repo

The first two are the same everywhere, so they live in shared/ and check_shared_files.py holds each copy to the original. The third is not: cargo fmt and prettier are not the same command, and pretending otherwise would put a tool in a repository that does not have it.

Each skips a check whose tool is absent, so a contributor without typos installed gets CI’s answer rather than a hook that cannot run. None of them duplicates a slow gate (Q-R57): tests, clippy and coverage belong to the task runner rather than the hook, because a hook that takes a minute is a hook people turn off, and one that is off enforces nothing.

Which of them the task runner actually holds is a per-repository answer, and it is not all of CI anywhere. lemonfiber’s just ci runs the suite and leaves coverage to just coverage; spec’s and brand’s run neither. That is fine — what is not is a recipe saying otherwise, which is the subject below.

Enabling is the hard part regardless, because core.hooksPath is per-clone local config that no commit can carry. Each repo sets it from a command a contributor already runs — just ci, npm install/npm ci, composer install — and a clone nobody has installed dependencies into is genuinely unprotected. What is enabled where, and where it still does not reach, is in shared/README.md.

actionlint would have caught a real bug

The workflow-injection guidance exists because untrusted event data in a run: block is exploitable. actionlint detects exactly that class, plus shellcheck on embedded scripts. It runs over every repo’s workflows.

just — the task runner

A justfile per repo gives named tasks (just test, just ci, just check) that run the same commands the pipeline does, so a contributor is not reading a workflow to find out what to type. It matches the CLI’s own ergonomics: discoverable, self-documenting, no hidden make magic.

The same commands is not all of them, and the difference is the subject of the command before a push is not CI. A repository driving its gate through npm or Composer keeps the steps there and puts the sentence in a justfile beside it, because package.json has no slot for one and npm prints none.

Astro Starlight — one docs site for the whole org

Roadmap M10 calls for a published docs site. It is website-docs.lemonfiber.app, and it renders this specification alongside each repo’s own documentation rather than restating either (ADR-0015). Starlight (Astro, OSS) builds it to a static site served from a Cloudflare Worker’s static assets, with Pagefind search that runs in the browser and no third-party origin. lychee and starlight-links-validator check every link, authored and mirrored, so the published site never ships a broken one.

git-cliff — changelog from history

Because commits carry structured trailers (Spec:) and follow a consistent shape, a changelog can be generated rather than hand-maintained. git-cliff (Rust, OSS) does this at release time alongside cargo-dist.

ESLint and Prettier — because the rules that matter are type-aware

Web work — lemonfiber-web and sdk-ts — lints with ESLint and formats with Prettier.

The rule set doing the work is typescript-eslint’s strictTypeChecked, which asks the type checker rather than reading the syntax tree: a floating promise, an any crossing a boundary, an await on something that was never a promise. Those cannot be answered from a parse, so a linter that only parses cannot implement them, however fast it is. This is the same bar the Rust side sets with clippy::pedantic and -D warnings, which is why the two repos are held to it identically (lemonfiber-web).

Prettier formats and ESLint does not — its formatting rules stay off, so the “one formatter, no arguments” posture (code-standards) holds with one tool deciding layout rather than two arguing about it.

Paired with @axe-core/playwright for the accessibility testing the contrast contract and G3 require. Both repositories that sweep drive axe through Playwright against a built site; neither runs pa11y, and nothing in the org does.

Anti-drift

The tooling is kept consistent across repos by construction, not vigilance:

  • Reusable workflows in the spec repo (hygiene, security, spec-check) are called by three-line wrappers in each repo. One definition each.
  • Dependabot config is one file per repo. It is the one thing here that is copied: Dependabot cannot extend a shared preset, so the copies are kept alike by review rather than by construction (ADR-0016).
  • .github repo supplies community files org-wide.
  • shared/ in the spec repo holds the files that must be copied because a tool or GitHub reads them from the tree it is given — the two lint configs, the hooks, the gate scripts and the brand assets a README shows. The shared-files job in the hygiene gate fails a copy that has drifted from the one here.
  • Two homes are not in shared/. A brand asset’s is the brand repository’s own original, named in shared/assets.sha256; CLAUDE.md’s is this repository’s own copy at the root, because the one link in it is relative to a repository root and would resolve to nothing from inside shared/.
  • Task-runner recipes are per-repo, because what each repository can run locally is different. What they may not do is claim to be CI while running less of it than CI does — below.

The distinction worth keeping straight: a reusable workflow is not copied, so it cannot drift. A lint config is copied, because the gate checks out the calling repository and lints that tree — the org’s copy never arrives. Those two need different machinery, and conflating them is how four different .markdownlint.jsonc files came to be live at once.

Adoption is declared, not inferred

Two members of shared/ are adopted rather than required. A repository gating on no open CodeQL alert is not asked for the gate script, and one whose contributors have not turned hooks on is not asked for the hooks. What is refused is carrying a copy that is not the current one.

That leaves absence carrying two meanings at once. Never adopted and adopted, then deleted arrive at the check as the same thing — a file that is not there — and it passes over both and reports that the copies match. Which is true of the copies it looked at, and reads as an account of all of them. One of those two meanings is a script that decides whether a branch merges, no longer running, in a repository whose pull requests still go green.

So adoption is stated in shared/adoption.toml and the check reads it. Where there was one answer there are now four:

The register says The repository holds Then
it carries the file the current copy passes
it carries the file a copy that differs refused, as before
it carries the file nothing refused, naming the file and the repository
it carries nothing a copy refused — a copy nothing declared is one nothing holds to the canonical version

A repository with no row at all is refused rather than passed over, because an unknown repository is the silence the register exists to end. The register is the only place that answer lives: a second list able to disagree with it would be the defect rather than the fix, which is why the per-repository half of turning the hooks on is a row here rather than a table there.

A register that is absent, unreadable or naming nobody refuses too. It is the input both checks are decided by, so one that did not arrive is a question that went unasked — and a question that went unasked has never been the same answer as nothing to report.

The first prose held to anything

shared/ covers configuration, hooks, gate scripts and images. It covered no prose at all, and the largest duplicated blocks in the org are prose: a licence rationale in 21 places, a footer in 16, the pointer to this repository’s contributor rules in 13.

CLAUDE.md is where that starts, because it is the cheapest possible case. It is one sentence — see AGENTS.md — it is the same sentence in every repository that has one, and there is no local variation to allow for. It was already byte-identical everywhere it existed, held there by nothing, which is a copy waiting to drift rather than a rule.

Required rather than adopted, unlike the hooks: GOV-R26 asks every repository for both files, the guide and the tool-specific name pointing at it. Read conditionally — if a repository has a guide, check the pointer — the one repository carrying neither would have been the only one this said nothing about, and it is the repository where an agent finds no guidance at all.

What the pointer may not become is a second guide. Anything about a particular repository belongs in its AGENTS.md, which this does not read and which is deliberately free prose.

The command before a push is not CI

Every repository has one command that stands for CI locally — just ci, npm run ci, composer ci — and none of them is it. Most of what a pull request starts here is forge-side: the citation gate, the sign-off, the subject, CodeQL, the secret scan, the label sync. A machine with a clone cannot run any of them, and no recipe ever will.

A recipe describing itself as everything CI runs is therefore saying something that cannot be true, and the cost is specific. A contributor who runs it, pushes and goes red learns that the command is not worth running — and then does not run it for the things it would have caught.

check_local_command.py holds the sentence rather than the coverage. A recipe may leave out as much as it likes; what it may not do is claim the opposite. Two shapes pass — the clause, which reads best when there is one omission, and the list, which reads better when there are six:

Everything CI runs bar the image check, which needs the network.

Every gate CI runs but backward compatibility, in order; run composer bc for that one.

The floor is the entry point. A repository with no local command, and one whose command nobody described, are both refused — the second because it is the state in which this has no sentence to read and would report that every claim in the repository is honest.

No file outgrows a reader

No tracked file in any repository is over 1,000 lines. Generated output counts: a generated file is read in every diff that moves it, by whoever reads a vendored copy, and by the generator downstream of it. One that outgrows a reader is split where it is generated, so the generator writes a directory rather than one long document.

Two kinds of file are not counted, and the list is closed. Lockfiles (*.lock and package-lock.json) are written by a package manager and nobody reads them line by line. Images and fonts are binary or, for an SVG, drawn rather than read.

One kind of file is held to the cap through other files. A file generated.py registers as generated from sources is read through those sources, and some cannot be split: a workflow that runs every check as one job is one file by what it is. It passes where every source is a tracked file within the cap, and is refused naming the source that is not.

hygiene / lines runs check_line_cap.py, which holds the cap and the list. It names each file over the cap, its line count and the cap. It judges, so it is required like every check that does (Q-R81, Q-R68).

A pin is a copy, and a copy goes stale

Anti-drift above covers what the tooling copies. It does not cover what a repository pins on purpose, and most of the arrows between these repos are exactly that: the stack and the web surface reach lemonfiber as submodules at a revision, lemonfiber-web reaches lemonfiber through a client it takes at a revision, every repo reaches the docs site the same way, and every uses: in every workflow names a SHA (ADR-0009). An exact revision is immutable, which is the point of it and also the failure: nothing about a pin changes because the thing it names moved on.

Renovate was what was meant to notice. It was configured on every repo and opened no pull request on any of them, so the true answer to “what is watching this pin” was nothing, for every pin here, from the first one written until Dependabot replaced it (ADR-0016).

Dependabot watches the uses: pins and the package manifests. It does not watch a submodule revision — that stays with the pins notice each consuming repo runs — and it does not watch a version written inline in a run: block, which is the gap above.

Two things went wrong under that within a day of each other, and both passed every gate:

  • The console was drawn through a client from before four of that client’s own bugs were fixed, so all four were live in it. A stale key read as “lemonfiber is not answering” — the client checked 401 and the binary answers 403, so the page reported the server down and never asked for the one thing that would have fixed it. The event stream never handed over an arrival. A refusal’s own sentence was discarded. The vendored contract predated a kind. Somebody building against the console found one of them again by hand, hours after it had been fixed upstream.
  • The same repository’s lockfile named a client it was not built from. package-lock.json sat behind package.json; npm ci installs what the lockfile names and the build then overwrites it, so the output was right and the record was wrong. That is the worse half. A wrong build fails somewhere eventually; a wrong record is what anybody reading the repository believes in the meantime.

Neither is a check behaving badly — both are the absence of one. A pin is not a version any scanner ranks, and a lockfile disagreeing with the manifest beside it is not a state the installer has any reason to object to.

So a pin is watched, and what a build resolved is held to what the repository declared.

The watching fires on the dependency’s own change rather than on a calendar. A weekly sweep is a floor, not a first notice, and seven days is long enough for the stale copy to be the one somebody installs. And the report names the commits not taken rather than counting them: most commits on a client change nothing its consumer reaches, so “behind by eleven” reads as an emergency where the list reads as a decision.

OPS-R48 already does this for one pin — when spec’s reusable workflows move, an automated PR bumps the @SHA in every consumer in lockstep — and the hygiene gate’s pins job already reports, in each repo, what shared revision that repo is running. Neither is peculiar to the release train or to shared CI. This is the general case of both.

Three notifiers carry it between the repos that pin each other. lemonfiber tells each SDK when the contract artefact it vendors has moved, and fails when an SDK could not be told (contract-moved.yml). sdk-ts tells lemonfiber-web on every push to its own main (sdk-moved.yml). Each consuming repo compares what it holds against what the repository it pins serves, and names what it has not taken, through the one comparison spec’s upstream-drift.yml defines, with a reader for each way a commit is taken: composer.lock, package.json, or a committed copy and the file recording its commit.

The two that dispatch fail rather than report success when they cannot reach the repository they are telling. A notifier that cannot notify, quietly, leaves the calendar as the only thing looking — which is the state all three exist to end.

Reporting was the half that did not work

The comparison was right from its first run and changed nothing. All three checks were red at once, correctly — both SDKs behind the served contract, the console behind the client — and stayed red for days with nobody acting on any of them. In the Actions list an advisory red and a broken gate are the same red X, and telling them apart meant opening the workflow file to read a paragraph saying so. The reader who would do that is the one who already knew.

The argument for reporting was that a pin is meant to lag: taking a contract change is a deliberate act that arrives as a diff somebody reads, so gating would refuse every unrelated pull request for the whole stretch between the server moving and the consumer catching up. The premise is true and the conclusion is the wrong trade. That stretch is not a grace period — it is the interval during which the repository ships against a copy it has been told is wrong, and holding it open is what the red run was supposed to prevent. The refusal is the point: it puts the catch-up in front of the next change rather than behind it.

So the check runs on pull requests, fails them, and is required on the default branch (Q-R68). What makes that bearable for an author who touched none of it is Q-R70 — the failure names the artefact that drifted and the command that regenerates it, in the failure itself. A gate whose remedy the author has to go and find is a gate that gets routed around, and “the contract moved” is not an instruction.

The refusal is not immediate. A repository that pins one moving as fast as this org’s do would otherwise be refused for the minutes between every upstream merge and the automated pull request that takes it, which is a gate nobody can get past rather than one that puts anything in front of anybody. So Q-R68 allows a commit 24 hours on the dependency’s default branch before its absence is a fault: long enough for an automated bump to open, pass and merge, short enough that a stalled one is red the next day. workflow-pins and upstream-drift both use it, and both read it from one named constant in spec’s scripts, so the number the gates apply and the number a reader is told cannot differ. Inside the window a commit not taken is a warning naming it and the hour it becomes a refusal. The window counts from the commit’s time on the default branch, which a squash merge sets to the moment it merged; a commit dated later than the run counts as past the window, so a date written by hand can only make the gate stricter.

Without the window, two pins moving at once would deadlock. A repository taking both the shared workflows and a client would hold two automated pull requests, each red on the pin only the other one moves, and neither could merge first. Inside the window both are green, and each merges on its own.

What counts is a commit that changed something the repository reads — the workflow file a pin names, the files a site renders — not every commit on the branch.

Where the catch-up needs a decision rather than a command, the gate waits, and Q-R71 requires the waiting to be written in the workflow that would carry it, with the reason, the work it waits on, and the date it is looked at again. A repository that gates and one that has not got there yet look identical from outside unless the second one says so — which is the same failure as the red X, one level up.

What needs a human, once

These can’t be scaffolded and must be set by hand after creation:

Action Where
Add SONAR_TOKEN secret Each repo running Sonar → Settings → Secrets
Create the fan-out environment with a 20-minute wait timer, deployments from main only spec → Settings → Environments
Enable Dependabot security updates Each repo → Settings → Advanced Security. Version updates need no toggle — .github/dependabot.yml is enough
Uninstall the Renovate GitHub App Org → Settings → GitHub Apps. It is still installed and no longer read

All are one-time and free. Everything else runs from committed config.

Requirements

ID Requirement
Q-R51 Every tool in the pipeline MUST be free for public repositories or open source; no paid tier MAY be required.
Q-R52 Code quality and coverage MUST run through SonarQube Cloud, which ingests the cargo-llvm-cov report; the applicable-code coverage gate is enforced per Q-R61.
Q-R53 SAST (CodeQL), secret scanning (gitleaks), and dependency vulnerability scanning (OSV-Scanner) MUST run in CI.
Q-R54 Workflow lint (actionlint), spell check (typos), link check (lychee) and markdown lint MUST run in CI.
Q-R55 A dependency-update bot’s pull requests MUST pass spec-check unattended; where the bot cannot emit the trailer, the gate MUST supply GOV-R12 for it, keyed on the pull request’s author and on nothing the pull request carries.
Q-R56 Shared CI MUST be reusable workflows sourced from the spec repo, not copied per repo.
Q-R57 Pre-commit hooks MUST mirror CI and MUST NOT enforce anything CI does not.
Q-R58 The docs site MUST render the specification from a pinned revision of spec, MUST link-check every page it publishes, authored and mirrored, and MUST be the only published rendering of the specification.
Q-R59 A public supply-chain posture check (OpenSSF Scorecard) MUST run on each repo’s default branch.
Q-R60 Any tool requiring a secret or external app MUST be documented as a one-time manual setup step.
Q-R68 Where a repository depends on another repository in this org at an exact revision, an automated check MUST run on every pull request, MUST fail where the pin has not taken a commit that changed something the repository reads from that dependency and has been on its default branch for longer than 24 hours, naming the commits it has not taken, and MUST be a required status check on the default branch. A check MAY fail sooner; one that lets a commit wait MUST read that window from a single named constant.
Q-R69 A lockfile or equivalent record of what a build resolved MUST name the same revision as the declaration it resolves, and CI MUST fail where the two disagree.
Q-R70 A check required under Q-R68 MUST name, in the failure itself, the artefact that drifted and the command that brings it current.
Q-R71 A repository that cannot yet satisfy Q-R68 MUST state that in the workflow that would gate it — the reason, the work it waits on, and the date the deferral is reviewed — and the check MUST keep reporting until the gate is turned on.
Q-R73 Where a shared file is carried by some repositories and not others, adoption MUST be declared rather than inferred from the file’s presence, and the drift check MUST refuse a repository that declares a file it does not carry and one that carries a file it has not declared, naming the file and the repository in each.
Q-R74 The adoption register MUST be the only record of which repository carries which shared file and MUST hold a row for every repository in the organisation; the drift check MUST refuse a repository it holds no row for, and MUST refuse a register that is absent, unreadable or names no repository rather than reading one as an organisation that has adopted nothing.
Q-R81 A tracked file in a repository MUST NOT be over 1,000 lines, generated output included; lockfiles, images and fonts are not counted, and a file the repository registers as generated from sources is held to the cap through those sources, each of which MUST be within it. The shared hygiene workflow MUST check every repository against the one cap and list, and its refusal MUST name the file, its line count and the cap.