✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (
ci-gate)Validation by Beadloom
doc_sync— same source assync-check.
CI Gate
The unified beadloom ci gate for the application domain.
Source: src/beadloom/application/gate.py
Specification
Purpose
Compose Beadloom's individual checkers into ONE GateResult with a single ok verdict, so CI is the only true enforcement point — identical for any author, whether Claude Code, Cursor, or a human. run_ci_gate runs every step in order and never short-circuits, so a later failure is never hidden by an earlier one.
Steps
run_ci_gate(project_root, *, fail_on, hub_exports, no_reindex) runs, in order:
reindex (unless
--no-reindex) — rebuild the index.lint —
lint --strict, architecture boundaries. Its summary carries what aforbid_importexemption excused — the10 crossings suppressed by an exemptionclause of the verbatim line quoted below — taken from the linter's own formatter so the Gate line cannot drift from the command it summarises (BDL-061.49). The clause is absent when nothing was suppressed. A SECOND clause names the population —lint PASS: 0 error(s), 55 warning(s), 10 crossings suppressed by an exemption, architecture-layers judged 357 of 365 live depends_on edge(s), the Gate's lint line verbatim asbeadloom ciprinted it on this repository on 2026-09-13 — taken from the same formatter (_population_note) for the same reason, and present at FULL reach as well as partial: a suppressed crossing is an anomaly, so its absence means there is nothing to qualify, while a population is the denominator of the counts beside it and357 of 365reads exactly like365 of 365when neither is printed (BDL-070 A4). A project that declares no layer rule gets no clause. There is deliberately no matching clause forrules_inert: an inert rule always emits a finding, so a non-zero count already flips this summary to the0 error(s), N warning(s)branch. This step is also the only one exported under a public name (lint_step):beadloom initruns it over the graph it has just written and exits 1 when it does not pass, so a divergence between whatinitwrites and whatinitrequires surfaces at init time rather than at the adopter's firstcirun (BDL-067, closing BDL-UX #192). ItsLintErrorbranch —rules.ymlpresent and unloadable — is the one place a finding'sruleis this step's own name rather than a rule's, with the loader's complaint inwhy, so the summary it carries is exported too, asRULES_CONFIG_ERROR: a caller that renders findings has to branch on it, andinitdid not, telling an adopter with a hand-edited rules file that a rule calledlinthad failed (BDL-067.6).sync-check — symbol-pair doc freshness; fails on stale and missing pairs, reports
unverifiedones asWARNrather than fresh, and states how many pairs a WORKING declaration EXCUSED, with the reason it was declared with (… 326 pair(s) fresh, 4 exempt — <reason>). The clause is absent when nothing was excused, so a project that declares no exemption keeps its line. A failing line counts stale PAIRS (3 stale pair(s)), the number of stale entriessync-check --jsonholds, and not documents.docs-audit — numeric/version fact freshness; fails on
stale>0, and states how much of the declared fact surface it covered.docs-quality — every check that reads the project's planning documents, assembled by
application/planning_report.pyso this step andbeadloom docs qualityreport ONE run (BDL-068 S1.4). Five writing-standard checks (BDL-061 S4b): a goal with a measurable clause, a decision carrying a reason, a risk carrying a mitigation, noPendingquestion inside anApproveddocument, and no unfilled template placeholder. Two structural ones:missing-sectionandempty-section, against the sections each document kind's own composed skeleton carries. Two about the axes:axes-without-a-seedandaxis-without-a-scope-decision. A required section no majority of a kind carries is reported once against the KIND, with its ratio and no file location, because the fix is in the template rather than in every document. Every finding is awarnand the step ispassedunconditionally, so a project whose documents predate the checks does not go red on upgrade. A project with no planning document is a NAMED skip that states the globs it looked under. Four states setnot_verified, and the step then reports WARN rather than PASS — unverifiable is not clean: a check that found no document with anything to read (NOT CHECKED: <checks>), a document KIND no content check enters (NO CHECK READS: <kinds>), a tabledecision-reasoncould not place as a table of decisions (NOT CLASSIFIED: N table(s), M row(s)), and a document nothing could decode (UNREADABLE: N). The second exists because the first is a global OR over the corpus and goes silent as soon as one document carries one row, so it cannot see a check that is blind on an entire shipped document kind. The third is BDL-UX #213: aReasoncolumn does not make a table a decision table, and the check names the tables it did not judge instead of reporting a measurement row as a decision with no reason. None of the four can redden a project — the step ispassedunconditionally. Measured on this repository, 2026-09-08, the step reports:docs-quality WARN | 259 document(s) read; measurable-goal 4, pending-in-approved 2, missing-section 102, routed-without-axes 12; NO CHECK READS: BRIEF, PLAN, SUMMARY; NOT CLASSIFIED: 12 table(s), 58 row(s)Measured again on 2026-09-02, after S1.4. The two axes checks read 0 documents because no planning document in this repository carries an
## Axessection yet, and the line says so rather than reporting them clean.The line prints findings, not what the check stopped deciding about. After
beadloom-mr2l.70re-scopedmeasurable-goal, 27 of the 150 newly-accepted goal statements name no witness either and this check now decides nothing about them. That limit is stated in the doc-quality SPEC and is not carried on this line — filed by reviewbeadloom-mr2l.19as a MINOR, and left as a stated limit rather than a silent one.issue-log — the issue log's numbers (BDL-068 S6). Three legs over a numbered log and the ledger that allocates its numbers:
duplicate-number(one number defined by two entries),unwritten-claim(a number claimed and never written into the log) andunclaimed-number(an entry past the ledger's floor holding a number no claim holds). Unlike its two neighbours this step BLOCKS, and the difference is the kind of claim each makes: a writing-standard finding is an opinion about prose a project may reasonably carry for a release, while a duplicate number is a reference that resolves to two entries and to neither — this repository shipped one for fifteen days across a CHANGELOG, a ROADMAP, eight test files and thirty-six tracker records (BDL-UX #187). Every leg's repair fits in the commit that trips it, which is what makes blocking fair. It cannot redden a project that has not opted in: the log is DECLARED underissue_log:in.beadloom/config.yml, and a project declaring none is a NAMED skip that states the key to add. A project that declared the block and mistyped a key reached that same skip untilbeadloom-rqma.7(BDL-UX #270); it now fails with0 leg(s) run; 1 entr(ies) declared, 1 unusable: issue_log (...), on the same rule asreadme-pairbelow and through the same reader. That fix covered THIS leg and not the twoissue-numbercommands, which kept the old answer untilbeadloom-rqma.8— so the entry was closed on the Gate and live on the command a person types, for one bead.not_verifiedcarries the honest half — before a project's first allocation the ledger has no floor, sounwritten-claimandunclaimed-numberenter no number at all and the summary saysNOT CHECKED:rather than reporting them clean.Since
beadloom-l9eethe line also carries the PARTIAL case, which is the one every adopter is in from their first allocation onwards:unclaimed-numberskips every entry below the floor by design, and the summary said nothing about how many that was. The line read240 entr(ies) uniquely numbered; 5 claim(s), floor 262over a log whose leg had entered five of those entries (BDL-UX #267).PARTLY CHECKEDstates the population, and it is not a finding — an unreached population is coverage, and making it one would redden every project that adopts the allocator with a log already written. Measured on this repository, 2026-09-09:issue-log PASS | 241 entr(ies) uniquely numbered; 6 claim(s), floor 262; PARTLY CHECKED: 235 of 241 entr(ies) are below floor 262, where unclaimed-number did not enter; 1 number(s) below the highest are stated nowhere and are unaccounted for, not freereadme-pair — the project's DECLARED document pairs, compared by shape (BDL-069 S4). This repository ships
README.mdandREADME.ru.mdand nothing held them against each other; on 2026-09-10 the Russian file carried a paragraph the English one had folded away, and the only number that differed between them was a line count — 362 against 360 — which nothing reads and which a translator wrapping two sentences differently moves by the same amount. The drift was found by a person reading the two files side by side. The comparison isdoc_sync.document_pairs, and it compares the sequence of blocks each document is built from rather than its text: the files are in two languages, so a text comparison is a check somebody has to switch off.Like
issue-logbeside it this step BLOCKS, and for the same kind of reason: a block one document has and the other does not is not an opinion about prose, it is a statement one language makes and the other does not, and the repair fits in the commit that trips it. A declared file that could not be read fails too — a declaration pointing at nothing would otherwise report0 finding(s)having compared no document at all, which is the false green this leg exists to remove.It cannot redden a project that has not opted in. The pair is DECLARED under
document_pairs:in.beadloom/config.yml; an adopter's translated README is their business and a check that guessedREADME.<lang>.mdwould turn somebody's green tree red on the upgrade that ships it. A project declaring none is a NAMED skip that states the key to add.A project that opted in BADLY is reddened, and this is the leg's own thesis. Four ways of mistyping the block — an absent
follower:, a misspelled one, a path resolving outside the project root, and a scalar where the list belongs — reached the skip above word for word, because the refusal went tologging, which the Gate renders none of. Measured at HEAD on a foreign two-package project,beadloom ciexited 0 on all four while the two READMEs of a project that had opted in were never compared. Each is a finding now, and the line carries the count:readme-pair FAIL | 0 pair(s) held, 0 block(s) compared, 1 finding(s); 1 entr(ies) declared, 1 unusable: document_pairs[0] (it has no `follower:` key; it has `source:`, `followr:`)The count is what tells "declared none" from "declared badly". A skip reworded to "possibly nothing was declared" would be the same defect in softer words, which is why the number of unusable entries is in the line and not the phrasing (
beadloom-rqma.7).issue-logwas fixed in the same act — one rule about what a misdeclaration costs, in one place, reading the declaration throughdoc-sync/components/config-declarations.That block above is what the leg prints since
beadloom-rqma.8and not before it.N finding(s)was taken from the comparison, which folds over the pairs HELD, so a refused declaration and a document nothing could read were findings the step returned and the line did not count. The leg printed0 finding(s)in the same run in which the Gate printed a finding about that leg, and this SPEC documented the1 finding(s)the code did not produce — the two disagreed for a fix cycle. The step builds one list, carries it and counts it, so the number in the line and the findings beside it are now the same expression.One case does not redden: a
.beadloom/config.ymlthat could not be read at all says nothing about whether the key is there, so the STEP skips, WARNs and names the file rather than reddening a project that may never have written it. That is a statement aboutstep_readme_pairand_step_issue_numbers, which is where it is tested, and not aboutbeadloom ci: a config file with a YAML syntax error ends the run ininfrastructure/scan_paths.pyduring the reindex step, with a traceback and no gate line at all, before either leg is reached. Measured on a foreign project, 2026-09-12, and filed as BDL-UX #287.The line names the population and not only the verdict, because
0 finding(s)alone cannot be told apart from a declaration that left nothing to compare. Two clauses qualify it and are absent when there is nothing to qualify:UNREADABLE:names each declared path nothing could read, andNOT COMPARED:counts the pairs whose two files were read and hold no block between them — that second case setsnot_verified, so the step reports WARN rather than PASS. The naming of pairs stops at three while the counts in front of it cover all of them, so a project with a dozen translations gets a line a reader can finish. Measured on this repository, 2026-09-11:readme-pair PASS | 1 pair(s) held, 109 block(s) compared, 0 finding(s); README.ru.md <-> README.md (109 block(s))The Russian file is the SOURCE in this repository's own declaration and the English one follows it, which is the direction the drift ran on 2026-09-10. Nothing in the check prefers one language; the pair states which document the other is held against.
A known limit, stated rather than silent. A half-written
document_pairs:entry — one with asource:and nofollower:, or a path resolving outside the project — is refused by the resolver with a logged warning and reaches this step as the same skip a project declaring nothing gets. The step cannot tell the two apart, because what it receives is the resolved pair list. Making the refusal visible belongs todoc_sync.document_pairs, not here.doc-spaces — the TO-BE → AS-IS relation (BDL-061 S5). Reports an epic with at least one closed bead that declared a graph node with no AS-IS document, plus a WORKING exemption that excuses nothing and a WORKING declaration the graph contradicts. Every finding is a
warnand the step ispassedunconditionally, for the same reason as the step above. A project with no TO-BE document is a NAMED skip that states the roots it looked under. FOUR states setnot_verifiedand the step then reports WARN rather than PASS, because each is a way to print no findings while having checked nothing: no tracker export was readable, no epic with closed beads declared a node, some epics declare none, and — sincebeadloom-mr2l.74— some epics the tracker does not name at all. The declare no node clause names the composition of that bucket when it holds more than the ordinary case, because a directory carrying no readable intent document (beadloom-mr2l.73) is not an epic whose author forgot to declare a node. That fourth state has its own clause rather than only the boolean:not_verifiedwas already True here for an unrelated reason, so a saturated signal said nothing about an epic that had left the export. The tracker is read from the committed.beads/issues.jsonlexport rather than from abdsubprocess, so the gate gives the same answer in a fresh CI checkout with no tracker installed — a check whose result depends on what is on the runner is not a gate. The line names the tracker it read, becausebeadloom docs spacesprefers the livebddatabase and the two can therefore differ on one tree at one moment. An epic that DECLARES a node and that the tracker cannot resolve is reported asepic_not_in_tracker: whether its work finished is unknown, and unknown is not clean. An epic that declares nothing is not reported that way — it is already counted in the declare no node clause, and one fact under two names makes the line unreadable.The exemption clause names two populations rather than one word for both.
N WORKING document(s) in the exempt spacecounts documents;M sync pair(s) excusedcounts pairs, and that number is the one the gate's own sync-check step measured in the same run — it is carried onGateStep.pairs_excusedand never recomputed, because one run printedexempt: 0fromsync-check --jsonand55 WORKING document(s) exempttwo lines apart about one tree. On this repository the 55ACTIVE.mddocuments live outside the documentation directory the indexer walks, so none of them is a sync pair and 0 is the honest pair count. When a project DECLARED the exemption, the line also states how many documents each declared half reached, so a one-line declaration covering 39 documents prints the number 39. A document whose kind places it in a space whose roots exclude it adds a final clause with its count (beadloom-mr2l.77).Measured on this repository, 2026-08-26, the step reports:
doc-spaces WARN | to_be 194, as_is 100, working 56; 17 node declaration(s) from 38 of 62 epic(s) with closed beads held against the AS-IS space; tracker read from .beads/issues.jsonl; NOT CHECKED: 57 epic(s) declare no node (4 carry no readable intent document); NOT CHECKED: 24 epic(s) the tracker does not name (BDL-001, BDL-003, BDL-005, BDL-006, BDL-007 and 19 more); 56 WORKING document(s) in the exempt space, 0 sync pair(s) excusedEvery count on that line is a moving denominator — this repository's own planning tree changes it — so the shape is the point and the numbers are whatever
beadloom ciprints today.One finding:
BDL-030declares a node while the tracker export has no record of it.BDL-061'scli-commandsdeclaration was the second one and closed when BDL-062.4gave that node an AS-IS document (docs/services/components/cli-commands/DOC.md).scope-check — did this branch leave the axes its work item declared? (BDL-068 S1.6). BRANCH-scoped,
<trunk>...HEAD, and that is the whole point: the tree is shared by several agents, so judging it would fail one agent's push on a neighbour's edit, while<trunk>...HEADis exactly what the pull request contains and what the approval was spent on. The trunk is theoptions.trunktheworking-branchguard already reads, preferringorigin/<trunk>when that remote-tracking ref exists — measured on this repository, a localmaintwo commits behind the remote made another work item's LANDED change read as this branch's work.passed=Trueunconditionally, likedocs-qualityanddoc-spaces: the check ships aswarnso a project whose work items predate## Axesdoes not go red on upgrade. A run that found no branch, no work item, no index, no## Axessection, no answer from git, or no changed path a node owns is a NAMED SKIP with its reason — never a PASS, because a comparison over an empty population has verified nothing. The summary states the findings, the paths judged, the paths no node owns and the declared rows nobody decided.config-check — agent-config drift (AgentConfigAsCode). Since BDL-061 S3 a drift carries its own severity:
errorblocks the step,warnis reported and does not. The summary has three forms accordingly —N drifted artifact(s),no blocking drift; N artifact(s) reported (warn), andagent-config in sync— because printing "in sync" over a reported finding is the false-green shape this epic exists to remove. The step also carries the mutation-SCOPE findings (BDL-061 S4b), each with its own rule name and severitywarning; they are computed BEFORE the step's database guard, because a declaration is checkable against the tree whether or not the index was built.doctor — graph integrity.
federate —
federate --fail-onwhen hub exports are supplied.
The docs-audit step (BDL-057 Layer 1) reuses beadloom.doc_sync.audit.run_audit — the same path beadloom docs audit calls — and fails the step when any documentation mention disagrees with a ground-truth fact (version, node/edge counts, language/framework counts, MCP-tool count, CLI-command count). The audit's false-positive masking and per-fact tolerances keep this honest; targeted exceptions live in .beadloom/config.yml (docs_audit.tolerances / docs_audit.ignore).
Its summary line carries the audit's own COVERAGE, not only its findings: 14 mention(s) fresh; 2/9 declared fact(s) verified, NOT VERIFIED: .... A bare 13 mention(s) fresh was measured on this repo to be thirteen restatements of ONE of nine declared facts — the line reported the checker's activity and read as a verdict on the documentation (BDL-UX #173). Coverage is reported, not enforced: silence in the docs about a fact is not a defect in the code, and a WARN every project would carry on every run would spend the channel sync-check needs for a genuinely missing baseline. beadloom docs audit --fail-if unverified>N is the opt-in for a project that wants every declared fact stated somewhere.
Since BDL-062 .3 the line also names the facts the audit declared no value for in this project: ..., NOT APPLICABLE to this project: cli_command_count. A fact that could not be computed used to leave no trace, so the denominator moved in silence — measured in-process on this repository, an unregistered CLI surface turned 3/9 declared fact(s) verified into 3/8 with nothing naming the fact that had left. The clause appears only when something was declined, so a project where every declared fact applies reads exactly as it did before.
Since BDL-068 .81 it also names the version tokens the audit declined to judge: ..., COULD NOT JUDGE 1 version token(s) naming git — unconfirmed here. A version belongs to the subject named beside it, and git is confirmed by the environment rather than by a file the project ships. A directory built by git archive HEAD — every clean room beadloom clean-room builds — carries no .git, so git 2.49.0 was compared against this project's version and every clean-room Gate run on this repository was rc 1 for one line of one document (BDL-UX #266). An unconfirmed subject is now unresolved rather than absent, and the token is reported instead of judged. This clause also appears only when something was declined, so a run in a git working tree reads exactly as it did before.
Each step reports a GateStep with PASS / WARN / FAIL / SKIP — never an ambiguous green — and its findings in the shared finding shape. GateResult.ok is True only when every step passed.
Nothing may pass by having less to check
Two step summaries were rewritten because they described the checker's ignorance as the code's health (BDL-UX #174/#175):
- sync-check names what it could NOT check. A pair whose doc, code file, or graph-declared doc is gone is
missingand FAILS the step — deleting a document was the cheapest way to satisfy the gate, and the count silently fell from 275 to 269 while every step printed PASS. A pair with no baseline isunverified: the step stays passed but printsWARNwith the count, because a project that cannot supply a baseline is not broken and must not read green either. When the committed declared-surface ledger records a larger surface than the run found, that is named too. And a pair a WORKING declaration excused is counted and its reason stated: theexemptverdict was added after this summary was rewritten and reintroduced the same shape, printing326 pair(s) freshwhere the same tree without the declaration printed 326 of 330 (beadloom-mr2l.76). Unverifiable, excused and clean are three states and do not print one word. - doctor counts the CHECKS that ran, not the findings.
run_checksreturns one entry per finding, solen(checks)counted problems: deleting a declared doc added anodes_without_docswarning and the summary read21 check(s) cleanwhere it had read 20 — a count that ROSE while the tree shrank. The summary is nowN check(s): 0 error(s), W warning(s), I info, and the word clean appears only when every check is OK.
A remediation the gate prints can be followed
The sync-check findings are where an adopter meets the gate first, and until BDL-069 one of their remediations could not be followed. Every stale pair printed "run beadloom sync-update <ref> to review and re-attest". Measured on a repository whose package document did not name one of its modules: following it exited 0, reported the pairs re-attested, and left ci red, because missing_modules reads what the document says and an attestation rewrites a recorded hash (BDL-UX #282).
doc-stalechooses its remediation fromattestation_clears(reason)(see the sync-check SPEC). A reason re-attesting clears keeps thesync-updateinstruction;missing_modules,untracked_filesand any reason nobody has measured printcontent_remedy(row)instead. For the measured case it reads:name journal in domains/ledger/README.md; re-attesting cannot clear missing_modules, …The
whynames the pair, as in this finding from the same measurement:ledger: doc out of sync with code (missing_modules: journal) — pair domains/ledger/README.md <-> src/ledger/core.pyA pair is a document AND a code file, so three files of one package give three pairs over one README, and without the code file those were three identical findings. A row that has no code file prints no pair clause.
doc-missingnames the file that is gone: the code file forcode_missing, where it used to name the document that was still there.doc-not-verifiedis chosen by reason too.no_baselinenames the form that attests an unverified pair,sync-update <ref> --yes --pair <doc_path>, because the baresync-update <ref> --yesclaims no unverified pair and attests nothing.sibling_symbols_changednames the file that moved and is not told to re-attest, which is bead.78's decision reaching the gate: itswhyused to say the index was rebuilt, a fact about a different pair.
The verdict names the room it was taken in
A verdict is true of the room it was taken in, and this project read one as a claim about the product four times — nine "green on the tree" reports measured on macOS against CI legs on Ubuntu, with the tenth measurement red on six of them, being the loudest. Since BDL-068 S3.2 GateResult carries a RoomCensus: the room the run was in, the rooms the project declares (interpreters from its packaging metadata, legs from every job of every workflow) and the ones this run did not enter. All three formats print it — under the verdict in rich, as a room object in --format json, and as one ::notice::room in --format github.
It is not a step, and naming it changes nothing. A step has a status, and the room makes no claim that can pass or fail: the same ok, the same exit code, the same findings. What it changes is that a green is answerable — a reader can see which of the declared rooms it covers. Do not read a room-naming verdict as a stronger one.
The verdict names what no step of it performed
beadloom ci does not run the test suite, and until BDL-UX #247 it never said so — while CLAUDE.md calls the pre-push hook "the full beadloom ci" and the coordinator skill calls it "the authoritative blocking backstop". Measured twice in one slice: a document change reddened two tests under a gate that returned rc 0, and a docs wave spilled an inline code span past a line under a gate that returned rc 0 over that tree twice, after which all six test legs went red on one assertion that reproduces locally in 0.07 s.
GateResult therefore carries a GateCoverage beside its room census: the verifications this project's pipeline declares that no step of this run performed, each with the command the pipeline runs for it and the workflow job it was read from. On this repository the block names three — the test suite, the style linter and the type checker. The second and third are the ones nobody had filed: the gate's own step is called lint and checks the architecture boundaries, not the source style.
Both sides are derived. What the run performed comes from its own step list, so a suite step added to the gate later removes the line by the same act rather than by somebody deleting a sentence. What the project verifies comes from its workflows, through the same load_jobs reader the room census uses. A project whose pipeline verifies under a name the vocabulary does not hold is told the population is empty with that limit named, never that nothing is left to run. gate-coverage (DOC) states the vocabulary and the four statements a run can make.
It is not a step either. Same ok, same exit code, same findings.
The verdict names who owns what it found
The branch that built this feature carried a red Gate across two waves of BDL-068 S6 — two stale docs owned by no bead in the running plan — and every gate owner in those waves had to be told by the coordinator, by hand, that the red was not theirs, so their reports would attribute the finding rather than discount it. A known red trains its reader to discount the next one, and the cost is never the red itself but the work of proving a second finding is real against a background that already holds one.
GateResult therefore carries a GateOwnership beside its room census and its coverage statement: one verdict per finding, held against the beads the tracker reports claimed while the run happened. owned names the beads. unowned says a node was derived and no claim covers it. unattributed says no node could be derived from the finding at all, which is a different absence and must not read as the same one. A tracker that cannot answer, and a project with no index, are a reason on the whole report rather than a page of unowned.
The claim is a bead, not the branch's approval. The work item's ## Axes answer whether a change is inside the approval, which the scope-check step of this same run already asks, and which every agent on one branch shares; a wave's plan names beads that have not started and beads whose wave is over. gate-ownership (DOC) states both trade-offs and the three routes a finding takes to reach a node.
It is not a step either, and the tracker is asked only when there is a finding. Same ok, same exit code, same findings; a green run attributes nothing and shells out to nothing.
Invariants
- Every step runs; the gate never short-circuits on the first failure.
- A skipped step counts as passed (it cannot block the build).
docs-auditblocks onstale>0and never counts an unverified fact as passing;sync-checksurface_drift,unverifiedand declared-surface-shrink findings are advisory and never fail the gate.- No step prints a count of something it did not check, and no step prints clean over a warning.
- No sync-check finding prints a remediation that cannot clear the reason it was printed for: a stale reason outside
REASONS_ATTESTATION_CLEARSis never told to re-attest, andtests/test_a_remediation_can_be_followed.pyfollows the printed instruction and fails if the verdict does not move. WARNnever changes the exit code: an adopter whose project is green today does not go red on upgrade, it only stops reading green where nothing was verified.- The room census never changes the verdict. It adds no step, no finding and no exit code, and
tests/test_gate_verdict_room.pyfails if it starts to. - The coverage block never changes the verdict either, and
tests/test_gate_not_run.pyfails if it starts to. It names a verification only when the project's own pipeline declares one this report can read. - The ownership block never changes the verdict either, and
tests/test_gate_finding_owner.pyfails if it starts to. A finding nobody claims is still a finding; a gate that went green because no bead owned a red would be the false green this whole slice exists to remove. fail_on=Noneselects the safe default federate set (breaking,drift,orphaned_consumer,undeclared_producer); the no-false-gate verdicts are never included.
API
Module src/beadloom/application/gate_step.py — the shape a step reports in, re-exported from gate.py so no caller's import path changed:
GateStep— one step:name,passed,skipped,findings,summary,not_verified, and thestatusproperty (PASS/WARN/FAIL/SKIP).gate_step_line(step) -> str— the step's own report line,[STATUS] name: summary._format_gate_richrenders it andbeadloom initquotes it, so the lineinitattributes tobeadloom ciis the linebeadloom ciprints (BDL-067.14).Finding— the shared agent-actionable finding shape.
Module src/beadloom/application/gate_declarations.py — what an unusable opt-in declaration costs a leg, for the two legs that are opt-in:
unusable_phrase(entries_declared, refusals) -> str— the; N entr(ies) declared, M unusable: …clause, or nothing.unusable_declaration_step(name, entries_declared, refusals) -> GateStepandundetermined_declaration_step(name, subject, refusals) -> GateStep.refusal_finding(name, refusal) -> Finding.
Module src/beadloom/application/gate_document_pairs.py — the readme-pair leg:
step_readme_pair(project_root) -> GateStep— the step, its line and the projection of what it found. The orchestrator composes it and renders none of it, which is the first slice of the per-leg extraction (beadloom-oew7).
Module src/beadloom/application/gate.py:
GateResult— aggregate:steps, the room census (room, aRoomCensus | None), the coverage statement (coverage, aGateCoverage | None), the ownership report (ownership, aGateOwnership | None), plus theokandfindingsproperties.Nonemeans nothing derived it, and a surface that was not told makes no claim.run_ci_gate(project_root, *, fail_on, hub_exports, no_reindex, performed_elsewhere=(), tracker=None) -> GateResult— run every gate step and aggregate the result.performed_elsewherenames verifications the CALLER runs beside the gate, in the vocabulary a step would use: the MCPcomplete_beadtool runs the suite itself and passes("tests",), so one run cannot report the suite as not run while that run ran it.trackeris the read port over the work tracker, supplied by the service that runs the gate because thebdseam lives in the services layer this one must not import; a run given none makes no ownership claim rather than reporting every finding as owned by nobody.
Module src/beadloom/application/gate_coverage.py:
derive_gate_coverage(project_root, *, performed) -> GateCoverage— the verifications the project declares thatperformeddoes not cover.gate_coverage_lines(coverage) -> list[str]— the block all surfaces quote.
Module src/beadloom/application/gate_ownership.py:
derive_gate_ownership(project_root, *, findings, tracker) -> GateOwnership— one verdict per finding, held against the beads the tracker reports claimed.gate_ownership_lines(ownership) -> list[str]— the block all surfaces quote.
Testing
Tests: tests/test_gate.py, tests/integration/doc_sync/audit/test_ci_gate.py, tests/test_gate_not_run.py, tests/test_gate_finding_owner.py, tests/test_f3_gate_coverage.py, tests/test_the_gate_blocks_each_break_class.py, tests/test_a_remediation_can_be_followed.py, tests/test_a_stale_line_names_its_pair.py; scenarios in tests/acceptance/features/remediation_that_can_be_followed.feature