✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 96% (
application)Validation by Beadloom
doc_sync— same source assync-check.
Application
Use-case orchestration layer. Sits between the interface layer (services/tui) and the domain layer (context_oracle, doc_sync, graph, onboarding). Its modules coordinate multiple domains plus infrastructure to fulfil a use case and hold no business rules of their own.
Layer order: services → application → domains → infrastructure. The application layer may depend on domains and infrastructure (legal top-down); it is never depended upon by a lower layer. Extracting these orchestrators out of infrastructure is what lets infrastructure stay domain-agnostic and restores the DDD Dependency Rule.
Features
Each feature has its own SPEC.md:
- Reindex — full + incremental index rebuild (
beadloom reindex). - Doctor — graph/data integrity checks (
beadloom doctor). - Debt Report — weighted architecture-debt score.
- Watcher — file-watch auto-reindex.
- Site Generation — the VitePress site generator (
beadloom docs site). - CI Gate — the unified
beadloom cienforcement gate. - Flow Guards — the named-guard primitive the agentic flow binds to (
beadloom guard). - Wave Plan — decide which beads may run at the same time, and build the clean room each of them measures in (
beadloom waves,beadloom clean-room). - Review Brief — hand a reviewer the change and the specification, and withhold the author's account until a verdict is recorded (
beadloom review-brief).
Specification
Modules
reindex/ — package (decomposed by cohesion in BDL-059 S4 into
models,rules_loader,indexing,enrichment,sync_state,change_detection,full,incremental, joined bytest_indexin BDL-074 C1; the package__init__re-exports the stable public + back-compat surface).reindex(root)performs full reindex: snapshot sync baselines → drop tables → create schema → load graph YAML → recordtests:declarations → store deep config → index docs → index code → resolve imports → load rules → analyze git activity → extract API routes → populate file index → index test files → build sync state (with preserved symbol hashes) → populate FTS5 → clear bundle cache → take health snapshot → store parser fingerprint. A layer rule is stored with itsexempt:entries since BDL-070 B4, because the architecture view reads its rule from therulestable and asks it which edges to draw red. The three suite rules of BDL-074 C3 (test_binding,test_import_boundary,scenario_binding) are stored whole as well, with their exemptions, so a reader of the table sees no stricter rule than the one that runs. A route is stored on every node whose source its file lies under by path component, throughinfrastructure.node_source.NodeSource, the rule git activity anddocs polishcall too. Until BDL-069beadloom-rqma.4,enrichmentmatched by string prefix and gavesrc/ledger/the routes ofsrc/ledger_archive/. The route store is the whole answer rather than a merge, so a node that holds no route loses the key and a stale route does not survive an incremental reindex. The file index moved AHEAD of the sync-state build in BDL-061.50 and the order is load bearing: a node's owned files are read fromfile_index, and a full reindex drops every table first, so populating it last left the FIRST build of a fresh index with an empty table and produced those pairs only on the second run.incremental_reindex(root)updates only changed files; detects parser availability changes via fingerprint comparison and graph YAML changes via_graph_yaml_changed(), triggering full reindex when needed. Backfillssymbols_indexedfrom the live DB (totalcode_symbolscount) so the result reports the true symbol total, not just the per-run delta (mirroring the #88 nodes/edges backfill). Since BDL-074 C1test_indexrecords the project's test files in their own tables (test_files,test_imports,test_overrides), binds each to a node by the mirror of its path, a node'stests:declaration or, since BDL-074 G2, its place inside a node's source, and rebuildsnodes.extra["tests"]from that binding in the four-key shape; the heuristic_store_test_mappingsstep is removed. Itsplacement_counts()delegates toinfrastructure.repository.count_test_files_by_placementsince BDL-074 C2, becausectxand the debt report state the same counts and neither may import the reindex. Since BDL-074 F1kind_counts()reads theother_kindfiles by recorded kind (count_other_kind_test_files), and the reindexTests:line names each kind with its count (72 acceptance step, 103 self-check) where it used to saybound by other means. Since BDL-074 G2 where the test files are and which files are tests is the test layout of.beadloom/config.yml'stests:block (roots, kind folders, patterns by framework, build-tool test trees,beside_code; sincebeadloom-2mj3.15the default roots aretests/,test/andspec/, joined by a top-level__tests__/inbeadloom-2mj3.17, each read where it exists, and a pattern with a/matches the end of a file's path); the files under a root are never indexed as code, a test beside the code is taken from the code scan, the layout is recorded asmeta.test_layoutwith the roots that exist and, asabsent_roots, the ones that do not, and an unusable key or a node'stests:prefix that binds nothing is a reindex warning. Both paths run it after the file index. An index without the test tables is rebuilt in full once, and a test-only change no longer reads asnothing_changed. See the reindex SPEC.doctor.py —
run_checks(conn, *, project_root=None)validates graph health with DB checks (empty summaries, unlinked docs, nodes without docs, isolated nodes, symbol drift, NOT-FRESH sync entries —staleandmissing, since "No stale sync entries" over a deleted document is the false green themissingverdict exists to end (BDL-UX #174) — source coverage gaps) plus an optional "Agent Instructions" check whenproject_rootis provided, comparing CLAUDE.md/AGENTS.md factual claims against runtime truth. Four of those claims describe the TARGET project, and until BDL-UX #183 every one of them was audited against Beadloom's state: version vsget_actual_version()(our__version__), packages vs a scan of<project_root>/src/beadloom/, stack vs the literal keyword set{python, sqlite}, and test framework vs the literal stringpytest. All four read OK on this repository by coincidence, and a TypeScript adopter with a correctCLAUDE.mdwas told their stack claim was missing keywords and their test framework was not pytest. They now read the project throughonboarding.scanner.project_facts— its declared version, its ownsrc/packages, itsflow.ymlstack, and whatever its manifests declare — and a fact the project does not declare isINFO … not verifiedrather than drift (unknown is not zero, and it is not a verdict either)._check_stack_claim()and_check_test_framework_claim()carry those two;_get_actual_packages()is gone, replaced bydetect_source_packages().get_actual_version()is unchanged and still returns Beadloom's own version — it exists to diagnose Beadloom's version drift (BDL-UX #92) and the defect was always the caller, never that function.nodes_without_docsreports three outcomes, not two (BDL-062.4): a node with no document isWARNING, a node whose graph YAML recordsdocs_absent: <reason>isINFOwith the reason printed — reported, never hidden, so a reader can disagree with the decision — and a node that records a reason while HAVING a document isWARNINGagain, naming the document, because a suppression that suppresses nothing reads as coverage it does not have. A blank reason is not a reason. The key needs no schema column: the loader stores any key it does not map intonodes.extra.debt_report/ — package (decomposed by cohesion in BDL-059 S4 into
models,config,collect,scoring,trend,render; the package__init__re-exports the public surface).collect_debt_data()aggregates architecture health signals from lint, sync-check, doctor, git activity, and the test binding: since BDL-074 C2 a node is untested when it carriesextra["tests"]with no bound test file, and while any test file is unplaced the count is withheld (0) andDebtData.test_population/DebtReport.test_populationsay why — a node with no bound test may still be tested by an unplaced file. Since BDL-074 G2 that sentence names the folders of the test layout the reindex recorded, and sincebeadloom-2mj3.15the population always ends with what a test file is read by — the patterns and the roots that exist, or, with none and tests beside the code read,and it lies beside a node's code, since none of the roots tests, test, spec, __tests__ exists(beadloom-2mj3.17, reworded bybeadloom-2mj3.19) — so "all N test file(s) placed" reads as all the files those patterns matched. The Rich report prints it under Test Gaps and the JSON carries it astest_population.compute_debt_score()applies a weighted formula producing a 0-100 debt score with category breakdown, severity classification, and per-node top offenders.format_debt_report()/format_debt_json()render the report.compute_debt_trend()compares against the last graph snapshot.DebtData.layer_populations/DebtReport.layer_populationscarry how much of its edge set each declared layer rule judged, in the sharedpopulation_phrasewording, because the error and warning counts are counts OVER a population and this collector is one of the two surfaces that callevaluate_allwithout ever building aLintResult(BDL-070 A4). The Rich report prints them ascounted over: ...under Rule Violations and the JSON carries them underlayer_populations; they are carried unweighted, because a statement about how much of the graph a count covers is not itself debt.watcher.py —
watch()monitors project files (graph YAML, docs, source) and auto-triggers reindex on changes usingwatchfiles. Graph changes trigger full reindex; other changes trigger incremental.WatchEventfrozen dataclass captures per-event metadata.DEFAULT_DEBOUNCE_MSconstant (500ms).site.py —
generate_site(conn, out_dir, *, project_root, federated=None, now_ts=None)is thedocs siteuse-case: it reads the indexed graph read-only and writes a VitePress content tree underout_dir(defaultsite/) — anindex.mdAbout home page rendered from the projectREADME.mdviasite_about.render_about(link-rebased; falls back to the architecture overview body when noREADME.md), aru/index.mdRU About page fromREADME.ru.md(omitted when absent; both About pages get the in-page bilingual cross-link/↔/ru/viacross_link_routes), anarchitecture.mdarchitecture page — the interactive Cytoscape+ELK compound graph primary view (public/architecture.data.json, delegated toarchitecture_view.py) plus the Mermaid counts/C4/health overview demoted to thearchitecture-diagram.mdfallback (the body that used to live atindex.md, BDL-046; the Mermaid graph was unreadable so the interactive view is now primary, BDL-060 S4 ext), adocs/index.mdDocumentation overview (BDL-046 BEAD-11: a short intro + one## <Group>heading per top-level docs group — Domains / Services / Guides / General — each followed by a single sentence that names its members as inline human-labelled TEXT, no link wall, since the full navigable tree is already the expanded Documentation sidebar), one page per node (delegated tosite_pages.py), the metrics dashboard (dashboard.md+dashboard.data.json, delegated to thesite_dashboard/package), the 🌟 landscape map — the interactive Cytoscape+ELK primary view (landscape.md+public/landscape.data.json, delegated tolandscape_view.py) plus the Mermaid fallback (landscape-diagram.md, delegated tosite_landscape.py), and.vitepress/config.generated.mjs(nav/sidebar). Before building the dashboard it backfills structural trend history fromgraph_snapshotsand records this run's honest metrics point (site_metrics_history.append_metrics_point) so the emitted trend series includes "now";now_tsis the injected ISO timestamp for that point (deterministic in tests; defaults to the current UTC instant in production — the only wall-clock read, and it lands solely in the append-only history store, never in the diffed dashboard fields). Beadloom produces, VitePress renders. Output is deterministic (sorted, stable frontmatter, no wall-clock in the diffed output) and never writes into the sourcedocs/. Returns a frozenSiteResultlisting every written path. Reusesgraph/c4.py(map_to_c4/filter_c4_nodes/render_c4_mermaid) for diagrams; reimplements no graph logic. Every emitted Markdown page is run through the Mermaid structural guard (site_mermaid_guard.validate_mermaid) before writing — a structurally broken diagram raisesMermaidValidationErrorand fails generation (closing the "build green ≠ renders ok" gap) instead of shipping a page that crashes the browser render.site_pages.py — per-node page rendering for
site.py(split out to stay under the domain-size limit).render_all_pages(conn)returns sortedNodePages; each page has summary, source, public symbols, a Relationships section, linked hand-written docs (rooted at/docs/so they resolve to the published copy undersite/docs/…), and an embedded scoped C4/Mermaid diagram. The Relationships section renders OUTGOINGpart_of/depends_on/usesedges as Markdown links to other node pages, then INCOMING relationships: Used by — the sorted, deduped union of incominguses+depends_onconsumers (who consumes this node; no separate "Depended on by" section) — and Parts — incomingpart_ofchild nodes. Incoming refs are link-safe (a ref with a generated page links to it, one without renders as plain text — never a dead link); self-edges are skipped; an incoming section with no entries is omitted (a leaf shows neither). Deterministic (sorted).site_nav.py — the generated VitePress nav/sidebar tree builders for
site.py(split out to keep the generator small).render_nav_config(conn, project_root)emits the full.vitepress/config.generated.mjsmodule exporting onlynav+sidebar(BDL-046 BEAD-11 dropped VitePresslocales— its global/x↔/ru/xmapping translated the whole menu and 404'd off/ru/— so there is a single shared EN sidebar and nonavRu/sidebarRu/render_sidebar_ru). Top nav is empty (render_nav→[]; BDL-046) — the VitePress default theme still renders the appearance toggle and local search regardless. The sidebar (render_sidebar(conn, *, docs_root, has_getting_started)) is a single ordered, link-safe tree: About (/) · Getting Started (/docs/getting-started, emitted only if that page exists) · Dashboard (flat) · Architecture · Landscape map (flat) · Documentation. The Architecture group iscollapsed: trueand apart_of-nested tree (service root → domains → features) with human-readable labels viahuman_label(context-oracle→Context Oracle), roots being nodes with no realpart_ofparent (aroot part_of rootself-edge is ignored so the root service isn't dropped); an "Architecture overview" entry stays on top and links to/architecture(the overview page). The Documentation group iscollapsed: false(expanded) and mirrors thedocs/directory tree (render_documentation_group_from_dir(docs_dir, *, collapsed)) as a nested, collapsible structure (each subdir a group, each.mda leaf link rooted at/docs/), led by an Overview link. Dashboard + Landscape map are plain{ text, link }entries (not one-child groups). Deterministic (sorted, byte-stable); no dead nav links.site_about.py — the README→About page transform (BDL-046).
render_about(readme_text, *, published_doc_slugs, repo_url, cross_link_routes=None)turns a project README's Markdown into the VitePress About/home page body by rebasing links (prose untouched, pure, deterministic, no I/O): adocs/<x>.mdlink whose slug is inpublished_doc_slugs→ the extension-less site link/docs/<x>; aREADME.md/README.ru.mdcross-link → if its lowercased basename is incross_link_routes(a basename→route map, e.g.{"readme.ru.md": "/ru/", "readme.md": "/"}), the link target is rewritten to that route (visible text kept) — this is the in-page bilingual About toggle that replaced the dropped locale switcher (BDL-046 BEAD-11); when no map is given the cross-link is dropped (text kept, back-compat); any other internal/relative target → an absolute GitHub URL{repo_url}/blob/main/<path>; already-absolute URLs, shields.io badges, and pure anchors are left untouched; the same rules apply to image targets; links inside code spans / fenced blocks are never rewritten. This lets the rewritten README be the bilingual front-door page (EN/, RU/ru/) without a hand-maintained duplicate.site_dashboard/ — package (decomposed by cohesion in BDL-059 S4 into
_common,gate_metrics,ai_activity,recommendations,alerts,status_cards,assemble; the package__init__re-exports the public surface). Showcase A, the AaC/DocAsCode metrics dashboard.build_dashboard_data(conn, *, project_root, federated=None)returns a deterministic, JSON-safe dict andrender_dashboard_md(data)renders the human page from that same dict (the front-end never invents a figure). Honest by construction: every number comes from the SAME code path as its gate —lint(count + severity breakdown viagraph/linter.lint),debt(debt_report.compute_debt_score+compute_debt_trend, serialized viaformat_debt_json),docs(coverage % +sync_statefreshness % + stale pair count, read-only),doctor(doctor.run_checkspass/fail summary), and an optionalfederatedrollup (per-service edge-verdict health + contract-verdict counts) reusing thefederateoutput verbatim. It also emitstrends— the recorded time-series fromsite_metrics_history.read_history(sorted byts; ONLY real recorded points — no interpolation, no fabricated samples; sparse at first is correct) —ai_techwriter— the honest "AI tech-writer activity" section (G9) read independently from the append-only run-record store.beadloom/ai_techwriter_runs.jsonthe CI harness emits (absent/empty/corrupt → an empty-but-present section, never an error):runs[]sorted bytswith per-run + cumulative docs-refreshed and input/output token spend (ONLY real recorded runs — same no-interpolation contract astrends),totals, and acost_estimate({usd, rate_usd_per_1m, is_estimate=True, label "est. @ $X/1M tokens"}) — token counts are FACTS from each record while the dollar figure is a clearly-labeled ESTIMATE at the configured_USD_PER_1M_TOKENSrate, never a hard cost (rendered by theAiTechwriterActivitywidget) — andrecommendations— a prioritized, actionable list built from the EXISTING gate data (one item per lint violation, BREAKING/DRIFT contract risks from the--federatedartifact, stale docs fromsync_state, and worst-debt nodes fromdebt_reporttop offenders); each item is{kind, severity, target, message, link}, severity-ordered (errors first) with deterministic tie-breaks, so the panel is honest by construction. For a critical-first UX it additionally emitsalerts— the attention-banner problems ({kind, severity, message}) shown IFF there is something wrong (BREAKING contracts →critical, DRIFT contracts / lint errors / doctor errors →error, stale doc-code pairs / high-debt →warn/error; the stale alert readsN stale pair(s)since BDL-069beadloom-yn6i, because its count is onesync_staterow per pair and the docs card beside it counts the same rows), severity-ordered (BREAKING leads) with deterministic tie-breaks; an empty list is the all-clear state — andstatus_cards— one threshold-colored card per metric group ({group, label, status, value, detail}withstatus∈ok/warn/error, the severity computed deterministically in Python so the front-end only paints the color).render_dashboard_mdemits only the page title + a short intro + the<ClientOnly>component mounts (no per-metric text dump, no<noscript>fallback) — the cards/widgets are the single presentation surface and read the honest figures fromdashboard.data.json(build_dashboard_data, unchanged).site_landscape.py — Showcase B, the 🌟 cross-repo landscape map.
build_landscape_data(conn=None, *, federated=None)returns a deterministic, JSON-safe dict (scope/nodes/edges) andrender_landscape_md(data, *, pages=None)renders a Mermaid diagram from it (never hand-drawn). With afederated.json(the F2federatehub output) nodes are the satellites and edges are the cross-repo links carrying the hub'sContractVerdict-style verdict verbatim; without it the map is the LOCAL contract graph —_local_landscapereads the repo's ownproduces/consumesedges, reconciles them bycontract_keyintograph.contracts.Contracts, classifies each to aContractVerdict, and renders one edge per producer→consumer coloured by that verdict (Beadloom's own site emits a singlebeadloom → vitepress-siteCONFIRMED edge; a repo with no contracts → an empty map). This is the real contract reality, not the structuraldepends_on/usesarch (which stays in the C4 overview). Edges are labelled by their verdict; a MermaidclassDefhealth overlay colours nodes (green = healthy, red = broken, grey = external/expected) and broken edges get a redlinkStyle. Clicks are page-aware: a node emitsclick <id> "/<dir>/<ref>"ONLY whenpages(fromexisting_page_urls(conn)) has a real generated page for it — a node with no page (asitenode, a foreign federated repo) renders without a click, so the map never links to a dead page. Every Mermaid id is prefixed (n_<sanitized>) so it can never collide with a reserved keyword (a node namedgraphbecomesn_graph— the label and click route keep the real ref). Since BDL-060 S4 the Mermaid diagram is the secondary/fallback view (landscape-diagram.md); the PRIMARY view is the interactivelandscape_view.pymap.landscape_view.py — Showcase B PRIMARY view (BDL-060 S4, G2): the interactive cross-service landscape.
build_landscape_view_data(conn, *, pages=None)returns a deterministic, renderer-agnostic, JSON-safe dict (schema_version/scope/nodes/edges/contracts) reconciled from the SAMEgraph.contracts.reconcile_contractspath the gate/report use — never a re-implemented surface — by reconstructing the contract-bearing edge dicts from each edge'sextra.contractblob (mirroring the satellite-export path), so each contract carries itsContractVerdict, protocol routing (AMQP exchange/routing_key/message_type, or GraphQL schema), producer↔consumer endpoints, the namedmissingbreak paths, and the DEEP field surface: the GraphQL typed Tier-Afields(exposed/referenced, S2) OR the AMQP body JSON-Schema (body.exposed/referenced, S3). Honest degradation: a contract with no declared surface carries an EMPTYfields/bodyblock (the view renders undeclared) — never a fabricated field. Nodes carrykind/group/health(worst incident verdict) + a pageurl(non-empty only when a real page exists, so a click never resolves to a dead page).serialize_landscape_view(data)is the byte-stable JSON (sort_keys);render_landscape_view_md(data)renderslandscape.md— the title + intro + the<ClientOnly><LandscapeMap></ClientOnly>mount + a static count summary (JS-off fallback) + a link to thelandscape-diagramMermaid fallback. The Cytoscape + ELK rendering, theme, pop-up, and filters live in the VitePress theme (site/.vitepress/theme/components/LandscapeMap.vue+landscapeTheme.js+useLandscapeData.js); ELK runs with fixed seedless options so layout is deterministic given the byte-stable data. The artifact is emitted undersite/public/landscape.data.json(VitePress copiespublic/to the dist root) for the runtimewithBase("/landscape.data.json")fetch.architecture_view.py — the interactive LOCAL architecture graph (BDL-060 S4 ext): the primary
architecture.mdview, replacing the unreadable Mermaid top-level diagram (demoted to thearchitecture-diagram.mdfallback).build_architecture_view_data(conn, *, pages=None, lint_violation_refs=None)returns a deterministic, renderer-agnostic, JSON-safe dict (schema_version/scope/nodes/edges) read from the SAME indexed graph the gate/report use (thenodes/edges/docs/sync_state/code_symbolstables) — never a re-implemented surface. Each node carrieskind/summary/layer(the node's own declared layer tag, minus the conventionallayer-prefix)/group/symbol count/doc_status(fresh/stale/none)/publisheddoc_links/pageurl/its compoundparent(thepart_ofcontainer)/and thebeadloom whylists (depends_on/depended_on_by) plus the DECLARED runtime coupling lists (uses/used_by); edges carrydepends_on(drawn solid) +part_of(containment → ELK compound parents) +uses(drawn dotted). Theusesrelation is authored in the graph YAML, not derived: a subprocess call or a file-format contract binds two nodes as surely as an import, but no import exists for derivation to find. It is kept SEPARATE fromdepends_onand never carries aviolationflag — crossing a process boundary to call a published interface is not a layering break the way an import is, and folding the two together would assert a binding that does not exist. The view used to filter these edges out entirely, so the authored edges already in the graph (everycli uses <domain>,mcp-server uses <domain>,reindex uses <domain>) were absent from the picture and a node coupled only that way read as an island. Which layers exist is READ, not written down: since BDL-070 A5 the module holds no layer tag and no rank table, reads the declared layer order from the indexedrulestable — the same graph every other read here goes through, so the site needs no second path torules.yml— and resolves membership throughgraph.rules.layers, the lookup the rule engine decides on.layerreads the node's OWN tag andlayer_rankinherits throughpart_of, because the card states what a node declares while the layout needs a lane for a feature that declares nothing. The edgeviolationflag is the RULE's verdict since BDL-070 B4: the module callsgraph.rules.layer_edges.flagged_layer_edges, so an edge is red here exactly whenbeadloom lintreports it, and the predicate this module kept —dst_rank <= src_rank, true for every same-layer edge as well as every upward one — is gone. The arrows the view drew red and the rule finds nothing against now render"violation": false; the measured count is in the site-generation SPEC, which is where a number about this graph is held against it. The flag stays OMITTED for an edge with an end in no declared layer, because the rule does not judge it and drawing it healthy would be the same overclaim the other way round. A layer rule declared over an edge kind other thandepends_onflags nothing here: this picture renders the verdict on dependency arrows, so such a rule is reported bylintand drawn by nothing. Honest degradation: a node with no doc gets EMPTYdoc_links(no fabricated link); no declared layer tag → emptylayer; a graph whose index carries no layer rule gets no lanes rather than every node in lane 0, and a project that carrieslayer-*tags while declaring no rule — the one shape whose rendered output moved in BDL-070 Release A, from four lanes to none — is told so at INFO with the number of tagged nodes (A8); thelint_cleanflag is OMITTED entirely whenlint_violation_refs is None(lint not computed) rather than faking a clean verdict.serialize_architecture_view(data)is the byte-stable JSON (sort_keys);render_architecture_view_md(data)rendersarchitecture.md— title + intro + the<ClientOnly><ArchitectureMap></ClientOnly>mount + a static count summary (JS-off fallback) + a link to thearchitecture-diagramMermaid fallback. The Cytoscape + ELK compound rendering (domains as parent boxes), layer-stratification colors, pop-up (kind/summary/layer/symbols/doc-status/dependency lists + base-path-correct doc links), blast-radius impact highlight, and filters (kind/domain/layer + show-only-violations) live in the VitePress theme (site/.vitepress/theme/components/ArchitectureMap.vue+architectureTheme.js+useArchitectureData.js); ELK runs with fixed seedless options so layout is deterministic given the byte-stable data. The artifact is emitted undersite/public/architecture.data.jsonfor the runtimewithBase("/architecture.data.json")fetch.site_mermaid_guard.py — the generation-time Mermaid validity guard (targeted structural validators, NOT a full parser).
validate_mermaid(text)returns a list ofMermaidIssuefor the two F4 render bug classes: (1) a flowchart/graphnode id that equals a reserved Mermaid keyword or has an illegal charset; (2) a C4Rel(a, b, …)whose endpoint is not a declaredContainer/Component/Person/System*node (a Rel to the boundary/root crashesdrawRels). An extensible validator registry; deterministic (issues in source order).site.generate_sitecalls it on every emitted diagram and raises on any issue.site_metrics_history.py — the metrics-history append-store backing honest dashboard trends. A tiny additive JSON log at
.beadloom/metrics_history.jsonofMetricsPoints (ts,lint_violations,debt_score,coverage_pct,sync_pct,nodes,edges,symbols).append_metrics_point(project_root, point)records one point perdocs siterun (thetsis supplied by the caller — nevernow()inside this module — so tests are deterministic; appending an existingtsoverwrites that point so a re-run does not double-count);read_history(project_root)returns the series sorted byts(only real recorded points, never an interpolated one);backfill_structural_history(conn, project_root)seeds structural counts (nodes/edges/symbols) from the existinggraph_snapshotshistory so the structural trend isn't empty on day one (idempotent; never overwrites a richer recorded point). Additive append-state, NOT a versioned artifact — no schema bump.site_published.py — Showcase C, the published validated documentation.
publish_docs(conn, out_dir, *, project_root)copies the REALdocs/**tree intoout_dir/docs/…preserving structure (the source of truth, rendered as-is) and injects a per-doc validation badge into the COPY only — the sourcedocs/is NEVER mutated (no AI prose-rewriting; that is the deferred F4.1). A generateddocs/index.mdlanding page (sorted links to every published doc) is also emitted so the/docs/nav target resolves.build_published_docs(conn, *, project_root)returns the deterministic per-doc inputs (PublishedDoc:status/reason/synced_at/ref_id/coverage_pct); the status comes from thedoc_syncengine viacheck_sync— the SAME code pathbeadloom sync-checkruns — so a doc the gate calls stale showsstaleon the site. The badge head is✅ fresh/⚠️ stale — <reason>for tracked docs; a doc tracked by NO doc-code pair is badged neutrally as📘 reference — overview/guide, not tied to a code symbol(an overview/guide is not a defect, so it is NOT called "untracked").inject_badge(prose, badge_body)wraps the badge between the stable<!-- beadloom:badge-start -->/-end -->markers so regeneration overwrites ONLY the badge region and leaves the authored prose byte-for-byte intact;render_published_doc(doc, prose)renders the badged Markdown. Fresh/stale badges showlast synced(the storedsync_state.synced_at, not wall-clock → deterministic) and the owning node's read-only source-coverage %; the reference (untracked) badge deliberately omits the coverage % line — that figure is the node's source coverage, unrelated to the prose, and printing it next to a not-tracked doc reads as a contradiction.planning_report.py — ONE composition of every check that reads a planning document (BDL-068 S1.4): the five writing-standard checks, the two structural ones, the two axes ones and the two route ones added in S1.5. The
docs-qualitygate step andbeadloom docs qualityeach assembled the run themselves before this, and two assemblies of one report can disagree about what was checked.applicableis stated for all eleven, because a check reported as0 finding(s)over a population of zero has verified nothing — and the two route checks report the WORK-ITEM count rather than the document count, because their unit is the folder. Classified as theplanning-reportcomponent node.doc_shape.py — the shape a project's documents are held to (BDL-061 S4b).
section_requirements(project_root)derives the required sections per graph node kind from the composed doc templates and is passed intocheck_sync;planning_documents/planning_document_globsfind the documents the writing-standard checks read (default.claude/development/docs/features/*/*.md, overridable bydoc_quality.pathsin.beadloom/config.yml);document_section_requirements(project_root)does the same for PLANNING document kinds (BRIEF,RFC, ...), derived from the same composed/templatescommand;shipped_placeholdersderives the placeholder vocabulary from that command's fenced blocks;shipped_decision_sectionsderives, the same way, the sections the shipped templates put a reason-carrying table under, which is howdecision-reasontells a decision table from a measurement table that happens to carry aReasoncolumn (BDL-068 S6, BDL-UX #213). The join lives HERE becauseonboarding(where the templates are) anddoc_sync(where the checks are) are peer domains that must not import each other. Classified as thedoc-shape-requirementscomponent node.doc_spaces.py — the TO-BE → AS-IS relation (BDL-061 S5).
check_spaces(project_root, *, spaces, known_refs, documented_refs, declared_doc_paths, beads_by_epic)is the pure core: it classifies every document into its space and reports an epic with at least one closed bead that declared a graph node with no AS-IS document — intent recorded, work finished, reality never written down. The join is read ONLY from the epicCONTEXT.md/BRIEF.mdRelated Files section, because that list is a declaration. The unscoped version was measured first, over 60 epic directories. It attributed the refstatusto nine of them whose documents merely used the English word, which is the false-positive class BDL-UX #169 and #190 already record against the audit scanner. An epic is a TO-BE DIRECTORY, not a directory carrying aCONTEXT.md: the narrower reading left four of this repository's 61 directories in no field of the report while their documents stayed in the TO-BE population, andunresolved_reasonsnow says which of three situations each unresolved epic is in — the document declares no node, the directory carries none of the configuredintent_documents, or the one it carries cannot be decoded (reported asintent_document_unreadable, since a document that is there and unreadable is a defect while a directory that is not an epic is not). An epic declaring nothing is counted as unresolved, never as clean, andrelation_checkedsays whether the relation had anything to relate at all. An epic the tracker does not NAME is a third state rather than an epic with no closed beads:EpicIntent.unknown_status_reasonsays which of the two ways its statuses are unknown,epics_unknown_to_trackernames them, and one that declares a node is reported asepic_not_in_tracker—bd closewrites only the local database, so an epic leaves.beads/issues.jsonlby ordinary use and the relation would otherwise stop checking it in silence.TrackerRead/read_tracker_exportcarry the statuses together with the source that answered, because the gate reads the committed export and the command prefers the livebddatabase. It also audits the WORKING declaration two ways: an exemption a project declared that matches no document — asked of EACH declared kind and EACH declared root, since liveness asked of a whole declaration is answered by its luckiest half and oneACTIVE.mdmade akinds: [ACTIVE, SPEC]line covering 39SPEC.mdfiles report nothing (working_exemption_inert, withworking_reachcarrying how many documents each declared half excused) — and a document the graph declares as a node's documentation while the config declares it ephemeral (working_declaration_contradicted). A document whose kind places it in a space whose roots exclude it is counted in that space and the disagreement is reported asdocument_outside_declared_root, one finding per kind with the count and up to five paths, because such a document used to be in no population at all (beadloom-mr2l.77).pairs_excusedis a count of sync PAIRS and arrives as an argument from whoever rancheck_sync, never recomputed here: one run printedexempt: 0and55 WORKING document(s) exempttwo lines apart about one tree, and a caller that ran no freshness check passesNoneso the surface makes no pair claim.spaces_report(conn, project_root, *, beads, tracker_source, pairs_excused)runs it over a live index;graph_facts(conn)supplies the three graph sets;jsonl_records/beads_by_epicread and group the tracker export. It lives HERE because the answer joins the graph, the tracker andinfrastructure/doc_roots.py— three readers no single domain owns. Classified as thedoc-spacescomponent node.intent_reader.py — the adapter that carries recorded intent into a context bundle (BDL-061
.87).read_intent(project_root, *, known_refs)reads the TO-BE space throughdoc_spaces.read_epic_intents— the same declaration join, never a second one — and returns the port typecontext_oracle.intent.IntentReading: the declarations, how many epics were read and how many of them declare any node at all.read_node_intent(conn, project_root)is the one call a surface makes, resolving the graph's vocabulary from the index first, so a backticked token naming no node is not mistaken for a declaration. It lives HERE and the policy lives incontext_oraclebecause a domain must not reach up into application; the port types are declared in the domain and this fills them. The tracker is deliberately NOT read:bd closewrites only the local database, so the committed export and the live tracker disagree on a branch and a bead status shown insidectxwould be confidently wrong where the work is happening — and the export costs 2.7 MB and 15 ms per cold bundle for a fact that does not change which epic declared the node. Measured on this repository: 61 epics read in 25 ms, against abuild_contextof 8.5 ms, paid on a cold bundle only. Classified as theintent-readercomponent node.mutation_scope/ — whether a declared mutation target could run a single mutant, and what a run over it produced (BDL-061 S4b + BDL-068 S3.1, CONTEXT Q5). Beadloom owns no mutation runner — the tool is the project's choice — so it owns the role duty, the scope convention and this check:
check_mutation_scope(project_root)reportsmutation-outside-source,mutation-target-missingandmutation-zero-mutants, allwarn, againstmutation.targetsin.beadloom/flow.ymlandscan_paths/languagesin.beadloom/config.yml. Surfaced byconfig-check(CLI and gate); it runs BEFORE the gate step's database guard, because a declaration is checkable against the tree whether or not the index was built.score.pyis the half that answers what a run PRODUCED:report_mutation_score(project_root, run)holds the counters a runner wrote against the declared targets and reportsmutation-target-unmeasured,mutation-run-zero-mutantsandmutation-counters-missing, allwarn. Three rules decide what the number means: a missing counter is reported rather than read as zero (read as zero it scores "0%", and a number is what gets pasted into a bead comment), a timeout counts as killed while a mutant no test covers does not (leaving that class out of the denominator is how a slice with no tests scores 100%), and the report names the ROOM it was measured in, derived from the platform and the interpreter rather than typed by the caller. The counter vocabulary is NAMES, not a tool: nothing undersrc/imports a runner. Since BDL-068 S3.3report_mutation_scorealso foldscheck_mutation_scopeover the targets the run is answerable for (filtered by--only), so the three SCOPE findings reach the score and not onlyconfig-check: before that a target naming a path the code had moved away from scored 100.0% at exit 0. Two further empty populations report there too — a run that produced mutants and reached a verdict on none of them, and a counter that is negative, which divided to "125.0% of -4 scored mutants". Surfaced bybeadloom mutation, which names the room on every report it produces including the one carrying no run. Since BDL-074 D1 a run can cover less than the whole scope and say which part:touched.py(changed_lines,touched_functions) maps a unified diff to the functions its lines fall in;change.py(diff_since,plan_change→ChangePlan) states a change's population since the merge base with a ref — the touched functions in the declared scope, the node owning each, the test files the binding ties to that node whatever placement bound them, and since BDL-074 G1 the acceptance step files whose loaded scenarios carry that node's@node:tag (NodeSelection.acceptance_tests, read byacceptance.py'sacceptance_files_by_nodefrom the literal pytest-bddscenarios()/scenario()paths a step file hands over) and theunplacedtest files alone as the runner's fallback (unplaced_tests, which replacedunbound_tests: a self-check or an acceptance step file is never the fallback). Since BDL-074 F1 the plan also carriestest_placementsandother_kinds, so theBinding:line states the unplaced countctxand the debt report state (context_oracle.test_binding.describe_unbound, over the recorded layoutChangePlan.test_layoutsincebeadloom-2mj3.15), with unowned files and each kind named beside it rather than folded into one number;survivors.py(read_survivors,survivors_by_node) lists survivors under the node owning their file;sample.py(wilson_interval,sample_interval) states the 95% Wilson interval of a score measured on a random sample.scope.lies_within(formerlyscore._is_covered) is the one path-containment rule both the score and the change use. Classified as themutation-scopecomponent node.active_table/ — package: the shared ACTIVE.md bead-status table parser/updater + the pure reconcile-from-bd core (BDL-053). It was a single
active_table.pyuntil BDL-068 S5, whose own docstring already needed an "and" to describe itself; the file moved withgit mvand__init__.pyre-exports the whole public surface, so no import path outside the package changed. Five modules, one responsibility each:row_ids.py(the bead id a row names, and what the row names when it names none),table.py(the markdown table, its Status column, one cell write),statuses.py(the state a Status cell states and thebdstatus it comes from),reconcile.py(the reconcile core) andstaging.py(what a reconcile may stage, which is never more than the commit already carries).split_table_row/is_separator_cellsare the markdown row primitives;set_active_table_status(path, bead_id, status)flips one bead's Status cell by whole-token bead-id match (the extracted MCP S4 behaviour, byte-identical —services/mcp_server.pyre-exports them for back-compat);bd_status_to_cell(bd_status)is the documentedbd-status → Status-cell map (closed → ✓ done,in_progress → in progress,blocked → blocked,open/ready → ready; unknown →None);resolve_row_bead_id(cell, bd_statuses, *, prefix=None)is the single place that maps the tracker'sbeadloom-mr2l.22onto a table's abbreviated.22, because comparing the two as whole strings matched nothing for the whole of BDL-053's life.reconcile_active_tables(project_root, bd_statuses, *, epic=None)discovers ACTIVE.md files (one epic or everyfeatures/*/ACTIVE.md), locates the bead-status table'sStatuscolumn by header index (3- or 4-col), and rewrites only the cells whose state drifts from the injectedbdstatuses — preserving a richer note when the state already agrees — returning aReconcileResultfor--checkvs fix. Since BDL-068 S5 that result states "no row at all" and "a row this run could not read" as two populations rather than one: on this repository the single number 79 became 41 unlisted beads and 38 beads a row names but no run could resolve, and one run reporting both of a single row is the defect the split removes. Best-effort: never raises, touches only Status cells (prose/Progress Log/other columns byte-preserved). Classified as theactive-tablecomponent node (its own DOC.md).gate.py —
run_ci_gate(project_root, *, fail_on, hub_exports, no_reindex)is the unified CI enforcement gate (thebeadloom ciorchestrator). It composes the existing checkers IN ORDER — reindex (unlessno_reindex) →lint --strict→sync-check→docs audit→docs-quality(BDL-061 S4b: the five writing-standard checks over the project's planning documents;warnonly, so it never blocks, and a project with no planning document is a NAMED skip). Its summary states four silences the finding count cannot:NOT CHECKEDfor a check that read nothing anywhere,NO CHECK READSfor a document KIND no content check enters,NOT CLASSIFIED: N table(s), M row(s)for a tabledecision-reasoncould not place as a table of decisions (BDL-UX #213 — aReasoncolumn does not make a table a decision table, and a measurement row judged as one is a false positive against honest documentation), andUNREADABLE: Nfor documents nothing could decode — each setsnot_verified, so the step reports WARN rather than PASS →issue-log(BDL-068 S6: the issue log's numbers —duplicate-number,unwritten-claimandunclaimed-number— which BLOCKS where its two document neighbours only warn, because a duplicate number is a reference that resolves to two entries and to neither rather than an opinion about prose, and every leg's repair fits in the commit that trips it. A project that declares noissue_log:block is a NAMED skip, so the upgrade shipping the step reddens nobody — while a project that declared the block and mistyped a key reached that same skip untilbeadloom-rqma.7and now fails with0 leg(s) run; 1 entr(ies) declared, 1 unusable: issue_log (...), BDL-UX #270 — andnot_verifiedsays when the ledger has no floor and two of the three legs entered no number. Its line also states the PARTIAL case sincebeadloom-l9ee—PARTLY CHECKED: 235 of 241 entr(ies) are below floor 262, where unclaimed-number did not enter— because that leg skips every entry below the floor by design and the summary said nothing about how many, so a clean list over five entries sat under a header naming 240, BDL-UX #267) →readme-pair(BDL-069 S4: the document pairs a project DECLARES underdocument_pairs:in.beadloom/config.yml, compared by SHAPE — the sequence of blocks each document is built from — and never by text, because the files are in two languages and a text comparison is a check somebody has to switch off. It BLOCKS for theissue-logreason rather than thedocs-qualityone: a block one document has and the other does not is a statement one language makes and the other does not, and the repair fits in the commit that trips it. A declared path nothing could read fails too, because a declaration pointing at nothing would otherwise report0 finding(s)having compared no document at all. A project that declares no pair is a NAMED skip, so the upgrade shipping the step reddens nobody, and a project that declared one BADLY is a different project: four ways of mistyping the block reached that skip word for word —beadloom ciexited 0 on all four, measured at HEAD on a foreign two-package project — and each is a finding now whose line carries1 entr(ies) declared, 1 unusable: document_pairs[0] (...), because what tellsdeclared nonefromdeclared badlyhas to be a count rather than an adverb (beadloom-rqma.7). Both legs read their declaration throughdoc-sync/components/config-declarations, so the rule about what a misdeclaration costs has one home; a.beadloom/config.ymlthat will not parse is the one case that still skips, WARNing and naming the file rather than reddening a project that may never have written the key. Its line states the population and not only the verdict —1 pair(s) held, 109 block(s) compared, 0 finding(s); README.ru.md <-> README.md (109 block(s)), measured on this repository 2026-09-11 — withUNREADABLE:naming each declared path nothing read andNOT COMPARED:counting the pairs whose two files were read and hold no block between them, the second of which setsnot_verifiedso the step reports WARN rather than PASS) →doc-spaces(BDL-061 S5: the TO-BE → AS-IS relation;warnonly, a NAMED skip on a project with no TO-BE document, andnot_verifiedwhen no tracker export was readable, when no epic with closed beads declared a node, when some epics declare none, or when the tracker does not name some of them at all — four ways to print no findings while having checked nothing, each with its own clause because the boolean saturates). Its tracker read is the committed.beads/issues.jsonlexport rather than abdsubprocess, so the gate gives the same answer in a fresh CI checkout with no tracker installed, and the line names it. Thesync-checkline states what a WORKING declaration EXCUSED, with the reason it was declared with: theexemptverdict was in no count any surface printed, which is the shape this summary had already been rewritten against once (beadloom-mr2l.76). That count is carried onGateStep.pairs_excusedand handed to thedoc-spacesstep, whose line names two populations apart —N WORKING document(s) in the exempt space, M sync pair(s) excused— because one word had stood for both and the two lines of one run said 55 and 0 (beadloom-mr2l.77) →scope-check(BDL-068 S1.6: the paths this BRANCH changes against its trunk, judged against the## Axesits work item declared;warnonly, and a run that found no branch, no work item, no index, no section or no owned path at all is a NAMED skip rather than a pass). Branch-scoped and not tree-scoped, because the tree is shared by several agents and<trunk>...HEADis what the pull request contains →config-check(AgentConfigAsCode, which also carries the mutation-SCOPE findings) →doctor(graph/data integrity; onlyERROR-severity checks fail the gate, so advisory WARNING/INFO checks never block — no false gate) → (whenhub_exportsgiven)federate --fail-on— into oneGateResultwhose.okis True only when every step passed. It ORCHESTRATES existing domain code; it reimplements no checker (the doctor step reusesdoctor.run_checks). Honesty invariants: no short-circuit (every step runs and ALL findings are collected even after an earlier failure) and no silent skip (eachGateSteprecordsPASS/WARN/FAIL/SKIP, whereWARNis a step that ran, found nothing wrong, and could not check part of what it reports on — unverifiable is not clean, BDL-UX #174/#175). Thedocs-auditline names a third population beside its fraction —NOT APPLICABLE to this project: <facts>— because a fact the registry could not compute used to leave the denominator without a trace, and an unregistered CLI surface was measured turning3/9 declared fact(s) verifiedinto3/8in silence. It names a fourth since BDL-068.81—COULD NOT JUDGE N version token(s) naming <subject> — unconfirmed here— for a version whose subject the ENVIRONMENT could not confirm in this directory:gitis derived from a.gitagit archiveexport cannot carry, sogit 2.49.0was judged 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 unresolved rather than absent, and the declined token is reported rather than silenced. Thelintline carries_population_notebeside_suppressed_note, both taken from the linter's own formatter so the Gate line cannot drift from the command it summarises; unlike the suppressed clause it is present at FULL reach as well, because a population is the denominator of the counts beside it and16 of 362and362 of 362read alike when neither is printed (BDL-070 A4). Findings are projected to the shared agent-actionable shape{kind, rule, severity, node, locations, why, remediation}(reused fromgraph/linter.py) uniformly across all steps, so--format json/githubare identical regardless of which step produced a finding.graph_reads.py — the application-layer read facade over the infrastructure the presentation layer reads (BDL-059 S2). Presentation code (
tui/) must not read SQLite directly (thetui-no-direct-infraboundary), so it consumes graph-index data — nodes, edges, symbols, hierarchy — through this facade, which delegates toinfrastructure/repository.py. Read-only; returns the repository's typed rows. Keeps the data-access seam in one place and the dependency direction honest (presentation → application → infrastructure). Since BDL-UX #172 it also passes throughanalyze_git_activity/GitActivityfrominfrastructure/git_activity.py: repairing that boundary rule's deadto:glob made it fire on the TUI's one remaining direct infrastructure import (tui/data_providers.py), and a second facade module for a single read would be indirection for its own sake.guards/ — package: the flow-guard primitive (BDL-061 S1).
evaluate_guard(...)turns a check outcome, theguards:block of.beadloom/flow.ymland the request context into aGuardVerdictwhose exit code a harness acts on.run_invocation(...)is the one boundary everybeadloom guardinvocation returns through, so a failure anywhere becomes a recorded verdict rather than a traceback. Checks read the world only through the ports incontract.py— the realbd/gitprobes live inservices/guard_probes.py, because the application layer must not import the services layer. See the Flow Guards SPEC. Two of the boundary's inputs are deliberately not ambient (BDL-061.36): the hook payload arrives as bytes and is decoded inside the boundary witherrors='strict', so an undecodable event is refused under every locale rather than only under a UTF-8 one; andpaths.pycatchesExceptionaroundPath.resolve(), so a symlink loop — which raisesRuntimeErroron 3.10–3.12 and raises nothing on 3.13 — is a stated refusal on every interpreter.firing.pybounds the record it writes (BDL-061.56): the activeguard-firings.jsonlrolls over atACTIVE_FIRINGS_CAP(2000) records intoguard-firings.1.jsonl, so--livenessparses a bounded file instead of one that grew with every guarded edit. Rotation loses no count — the outgoing firings are folded into a carried summary holding, per guard, the count, how many reached a verdict, and the first and last moment and outcome, which is every inputliveness.pyreads — sofired_count,never-firedand the last outcome are the same numbers after a rollover as before it. What it costs is per-firingwhytext older than one generation, andGuardLiveness.carried_countsays how much of a count rests on the summary rather than on readable lines. Two modules answer the binding's own coverage (BDL-068 S4, BDL-UX #170):shell_targets.pyreads what a shell command line says about itself — the program it runs, and the write targets a declared set of write shapes names, stated as a lower bound — so a shell edit resolves toPathScope.UNDETERMINED, matches no exclusion and carries the undetermined write set intonot_coveredinstead of reading as a cleanpass; andsurface.pyreports what fraction of the write paths the emitted role adapters grant is named by a registered matcher, deriving the matchers from.claude/settings.jsonand the population from each adapter'stools:line, reporting a tool it cannot classify as unclassified and an unreadable source as unresolved rather than as an empty population.BindingSurface.describe()has three sentences and not two for the reasontyped_surface.pybelow has three: an empty population is not an unreadable one, and0 of 0 write path(s) boundread as full coverage in the instrument built to report that class (BDL-068 S4.x, BDL-UX #239).coveredisNonein both non-fraction states andunresolved/nothing_to_checktell them apart. The report also names the corpus it answered about — the artifacts on disk — becauseonboarding.role_dutiesanswers the neighbouring question of the composition and the two looked like they agreed (#241). Binding the shell tool also changed what the firing record HOLDS, which is a separate question from how often it is written (beadloom-0mdo.43): a command line is reduced at the one door the context is built at (hook_payload.shell_command_context, reached from the harness payload and from--context command=...) to the program and the derived write targets, and the line itself reaches no verdict and no record. Reduction rather than redaction, because redactingKEY=valueand header-shaped operands is a denylist and the next credential arrives in a spelling nobody enumerated. Measured on this repository: of the 1 999 firings in one rotated generation, 1 927 had stored the line they fired on — 2.0 MB of one machine's shell history in a plaintext file inside the project directory; the same firings replayed through the reduction are 557 bytes a record rather than 1 007. It does not reach a record already written — nothing rewrites the file it is evidence in. Two of the six outcomes mean the guard did not answer, and BDL-UX #254 separated them by what it could not answer ABOUT:erroris a target the guard refuses to interpret, which stops that edit at exit 2 and stops nothing else, whileunresolvedis an inability the guard has about ITSELF — its own code will not import, itsflow.ymlwill not parse, the evaluation crashed, no project could be located — which warns and PERMITS, at 1 under a harness and 3 from a shell. Measured live in S5: agit mvleftservices/bd_seamwithout an__init__.py,guard_probes.py:79imports it to reach the tracker, andBash,WriteandEditall returned the sameImportErrorat the blocking code while the remediation asked for the write that verdict had just disabled — cleared only by a heredoc typed outside the session. The surface was not narrowed to fix it, becauseBashbelongs on the matcher; the verdict on inability moved instead. Permitting is not passing: the outcome is named on stderr besidePERMITTED_UNGUARDED, is recorded, and does not clearnever-firedin--liveness, whereis_unansweredis the one predicate both the liveness rule and the rotation summary read.waves/ — package: the wave decision (BDL-061 S6).
plan_waves(records, *, conn, overrides, today, environment)decides which beads may run at the same time from the code-level independence of their node scopes: a bead declaresrefs: <ref_id>at the start of a line in the tracker, the scope expands downward throughpart_of, and a pair is serialised for exactly one named reason —blocked_by_bead,unresolved_scope,shared_node,shared_file,dependency_edgeoroverride_serial. It DECIDES rather than advises, because an advisory wave shape is prose a model may ignore. A bead whose declaration cannot be read is serialised against every bead: an unknown scope is not an empty scope, and an empty one compares independent of everything. The parser fails in that direction on purpose (beadloom-mr2l.83): four unresolved reasons — no declaration, a ref the graph lacks, arefs:written inside a sentence, and a second ref written without a comma that the graph confirms is a node — each carry their own remedy and each serialise, because a wave shape is acted on and a parser whose errors widen a wave is worse than no parser. The declaration is parsed in one place (parse_declaration) AND composed in one place (compose_declaration), shared bybeadloom waves,beadloom review-briefand the MCPbead_contexttool: the four tracker fields are joined with newlines, and a caller that joined them with a space put the next field's first word behind a danglingrefs:header. A human outranks the computation throughwaves.overridesin.beadloom/flow.yml, each entry carrying a reason and an exit condition (the sameexit_condition_deadlinethe guard exclusions use) and reported with the number of decisions it changed — an override that changed none is a finding. Every wave states the seven media it shares no matter what shape is chosen (the graph the plan is derived from, the working tree, the commit gate, the landing order, the focus document, the doc baseline, the tracker's id space), names one bead as thegate_ownerthat measures the combined tree — because that step used to be in nobody's bead (BDL-UX #181) — names the clean room each of its beads owes asroom_for(bead_id)=room-<bead-id>, so a room can say whose it is rather than being a shared directory with a reassuring name (BDL-UX #235), and CHECKS each medium's plan-time precondition, so a medium that fails or that nobody measured is a finding rather than a line of prose printed beside the shape. Statement and check alike are unconditional on wave size since BDL-UX #228:wave_sizeis the width of one plan and not a claim about solitude, and thenot_applicableverdict a serial plan used to produce was silence exactly where nobody was already thinking about the risk — roughly twenty single-bead waves across two epics carried the discipline by a launch prompt alone. The six file-observed preconditions arrive as aWaveEnvironmentgathered by the command, so the decision stays runnable without git, without a repository, without a hook and without a scaffolded flow; the seventh compares each bead's title against the id the tracker allocated, which is the comparison BDL-UX #171 needed and nobody made. Thefocus-documentmedium is BDL-UX #257: this plan resolves a bead to the nodes and the SOURCE FILES its code occupies, so two beads can hold disjoint code scopes and one shared DOCUMENT and the plan reports0 serialisationstruthfully about the wrong population — measured on this project's own S6, where one bead's hunk indocs/domains/application/README.mdlanded inside another bead's commit, and again where four concurrent beads all wrote into oneACTIVE.md. It is a medium and not a serialisation because it cannot be one:docs.ref_idholds at most one node per document andshared_nodefires on any ref intersection first, so a serialisation derived from document ownership adds nothingshared_nodedoes not already produce. The population is DERIVED —Routing.shared_kindsis the intersection of the document kinds every route of the composed/task-initwrites, and the folder is the work item's own from the same branch read the commit gate makes — and the precondition asks whether that document carries a row for each bead of the plan, because a bead with no row of its own writes into the shared prose and its edit is committed by whoever gets there first. Thegraph-filesmedium is BDL-UX #261, and it is the same shape met one level down: the graph is what this plan derives every serialisation FROM, and a bead that adds a node writes it, so a derivation cannot describe its own input by asking it. Its check compares the node population the graph files declare against the one the index resolved these scopes from — a difference means the plan was computed from the older graph — and its pass states the half no plan can observe, that the node a bead is about to add is in no graph the plan could read. The serialisation the entry sketched was measured and declined: one file holds every one of this project's 100 nodes, so it fires on every pair and collapses each wave to a wave of one (BDL-UX #245's failure mode) against a real write rate of 8 of 55 commits, and the primitive that would remove the medium instead is one writer per file — one graph file per node, the shapebeadloom-0mdo.66already took at the boundary for issue numbers — which changes every adopter's.beadloom/_graph/layout and is filed rather than taken. Thelanding-orderprecondition islanding.lock_sites(invocations): the command reads the composed flow artifacts an agent is HANDED, the seam's onebdgrammar parses them, and this module reports every instruction ofbd merge-slotwhose call form grants less than it is relied on for -- anacquirewith no--holder(one tracker actor for every role, so the holder cannot be told from the claimant), areleasewith no--holder(the one form bd does not check, which frees a live neighbour's hold and reports success), and--wait(which appends the caller to a queue nothing drains and returns at once). Judged by the FLAGS of the invocation and never by the prose around it, because a check reading English for the promise "blocks until free" would repeat the keyword-proximity class already filed three times against the docs audit; a subcommand the derivation has not measured is reported asunknown-formrather than passing. Measured on bd 1.0.4 in an isolated rig with every exit code read without a pipe: the primitive itself is sound (one winner in each of four rounds of eight simultaneous acquires, andrelease --holderrefuses a caller that is not the holder), so what BDL-UX #194 and #237 both found was our call form, filed nine days apart by two agents that had never met. Since BDL-068 S5 this module carries no grammar of its own:beadloom-0mdo.51generalised it to everybdsubcommand and homed it at the seam, so the tree holds one grammar and one judgement of the lock rather than two derivations of the same kind. A declared ref is held against the work item's recorded table under five verdicts since BDL-068 S6, not four:approvedis the KEPT rows and nothing else, because a node owning a file theDerived byfield ran over reached the approval without anyone deciding it -- six nodes on this repository, two of them carrying rows that sayno(BDL-UX #250) -- and a swept node nobody ruled on isswept_no_scope_decisionrather thannot_derived, which would say the derivation never reached it. Theunguarded_axisremedy names a PER-BEAD derivation for the same reason: the work item's axes are the UNION of its slices' and a bead's scope is a SUBSET, and the remedy that prescribed the union for every bead would have made every pair share a node and collapsed every wave to a wave of one (BDL-UX #245). Records arrive as DATA, never a tracker handle, so the application layer does not import thebdseam. Since BDL-068 S6 the package also BUILDS the room it names:build_room(*, bead_id, project_root, parent, carry, rebuild, extras, environment)derives the path from the bead, creates the directory rather than entering one, and refuses a directory this run did not create — an existing room is left byte-for-byte as it was, andrebuildreplaces a room whose.beadloom-room.jsonnames that same bead rather than refreshing it. A rebuild reads the REQUEST out of that record — the carried list and the extras the caller pinned — because naming the files is the correct path and it was retyped on every rebuild, measured at 16--carryflags twice on one bead, and what an agent reaches for under that friction is copying files into the live room, which is #243 again. What is reused is the LIST and never the content: the files are copied from the working tree at build time, and an option named besiderebuildREPLACES its remembered counterpart, so the remembered list cannot grow into the "everything that differs" mode that deliberately does not exist. One rule decides what may be remembered — a rebuild must never silently produce a room whose verdict is greener or less isolated than the one it replaces — and it settles the two cases opposite ways: extras the caller PINNED are reused, because forgetting them widens the room to the legs' union and that is 0 mypy errors where the pinned leg reports 82 (BDL-UX #236), while a set the legs derived is derived again andenvironment=Falseis recorded but NOT reused, because a remembered decline hands back a room whose verdict the machine decides (BDL-UX #256) with no way to ask for anything else short of deleting the room. Naming was not enough twice: two agents of one wave reached one directory (BDL-UX #235), and files copied into an already-indexed room postdate its own doc-freshness baseline, measured assync-checkexit 2 withstale: 2against a change that is clean atHEAD(BDL-UX #243). It carriesgit archive HEADplus the files the caller NAMES, with no "everything that differs fromHEAD" mode, because on a shared tree that set holds the neighbour's work.room_invocation(path)hands back thePYTHONPATHan editable install needs so the suite imports the room's source and not the tree's, and the record STATES the optional extras that interpreter has, because those and not the files decided 0 mypy errors against 82 on one code base at one commit (BDL-UX #236). Since BDL-UX #256 the room also HOLDS that interpreter:room_env.pycreates a virtual environment inside the room and installs the room's own sources into it, so a verdict is no longer decided by whatever the machine happened to have — the name isolates the files, and nothing was isolating the import path or the environment. The extras it installs are the UNION of every extra any leg of the project's workflows installs, read from the typed install step (rooms.leg_installs) rather than from the satisfied set, which needs the analysed distribution installed under the running interpreter and is therefore unresolved for a project this tool is merely pointed at. The union rather than the commonest set, because the modal reading is wrong on this repository — four of the eight installing jobs installdev, languagesto build a site or run a release gate and two run the suite — and because the two errors are not symmetric: a missing extra removes tests from a run without failing it while a surplus one removes nothing, measured at 9 MB and no time over.[all,dev]. Failing to build one is a FINDING and never a refusal, since the files are isolated either way and what a reader must not do is assume; andinterpreter.extrasis then read off the room's own interpreter throughinstalled_extras(project_root, search_path=...), because recording this process's extras on that room's record would state the extras of an environment no verdict was taken in. Paid per room and never cached —uv venv0.082 s anduv pip install -e1.07 s for 184 MB apparent, under half a percent of a seven-minute suite — because an environment kept outside the room and reused is a directory two rooms share, which is BDL-UX #235 again. Since BDL-UX #283 a plan is also compared against the beads already IN PROGRESS under its work item (running.py): the beads it decides over are ready ones and a running bead is not ready, so--parentonce printed0 serialisation(s)while a bead of the same epic ran and conflicted with the one being launched. A conflict with running work is reported apart from the plan's own serialisations, because it does not order the plan's waves, and a running bead whose record could not be read is the findingrunning_not_compared. See the Wave Plan SPEC. Classified as thewave-planfeature node.review_brief/ — package: what a reviewer is handed (BDL-061 S6).
assemble_brief(conn, record, *, assignment, changed_paths, measured_since, notes, scenarios, project_root, branch, commits)builds a reviewer's input from three things and no fourth — the assignment (the bead's title and description), the specification (the graph's documents for the declared nodes plus the acceptance scenarios tagged@bead:), and the change (every path differing from the base ref, each carrying the node that owns it; the window is the BRANCH, not the bead, so thechanged-outside-scopefinding names what it measured over instead of claiming a per-bead attribution the commits do not hold). The author's comments are counted, never printed: a review that reads what the author said it did is not an independent check, and hidden-profile groups scored 17-36% against ~100% for a single holder of all the facts (BDL-UX #155 C).release_notes(notes, *, bead_author)ends the withholding once a verdict is on the bead —REVIEW PASSED:,REVIEW ISSUES:orREVIEW FINDINGS:, colon included, opening the comment's first non-blank line — so the measurements and deliberate deferrals in those comments are read AFTER the reviewer's own judgement is recorded rather than never. The author of the verdict comment is compared with the bead's assignee and the answer is reported rather than enforced: a self-recorded verdict still releases, prints why its independence cannot be established, and costs the run exit 1, because in this repository every role writes under one tracker identity and a gate nobody can pass is bypassed rather than obeyed. The split is description-against-comment because it is structural; claim-against-measurement would need a judgement no mechanism can make. A change nobody could measure comes back aschange_measured: falsewith a finding, never as an empty change set. Scope is resolved throughwaves.scope.resolve_scope, the one parser of therefs:grammar. The brief also states what is REACHABLE rather than what was withheld (BDL-068 S2): four channels — bead comments, the documents of the work item the branch names, the commit bodies ofgit log <base>..HEAD, and the launch prompt — each either inspected or NAMED as one this command cannot inspect, because0 withheldwas true of bead comments and was read as a claim about the reviewer's knowledge while the account reached three reviewers throughACTIVE.md, through the commit bodies and through a prompt nothing here can see (BDL-UX #204, #212, #219). The document population is DERIVED from the composed prompts themselves — every role inROLE_NAMESand every shipped command fragment, composed for this project'sflow.ymland including its project layer, matched by shape rather than by spelling — so a team that names its own document in.beadloom/flow/roles/review.mdmoves the report by that act and by no other. It raises detectability and closes nothing: the review protocol itself sends a reviewer to the diff. The bead-comment channel NAMES the bead its count was taken over and says the beads that made the change are neither read nor counted, because on a wave-structured slice the bead a brief is for is a review bead and0 item(s)was measured beside 31,544 characters on the two beads that made the change (BDL-068 S2 review, Major 1(a)). TWO CHANNELS THE REPORT DOES NOT NAME, both measured and filed: the tracker export inside the reviewed diff, which the brief's own change inventory lists and then tells the reviewer to read — 16 added record lines, 30 author comments, 81,270 characters on that same review — and behind it the slice's sibling beads (BDL-UX #229); and the work item's documents on a branch whose name carries a suffix after the key, wherefeatures/BDL-068-S2S3names no work item and the channel read NOT INSPECTED while the reviewer was reading RFC.md and CONTEXT.md out of that folder (BDL-UX #230, fixed indeclared_scope.pyor nowhere). Four channels is what the report states, not what exists. Aflow.ymlthat will not parse costs the documents channel and not the brief:prompts_naming_documentsanswersNone— which is not{}— and the channel reports NOT INSPECTED, because naming a malformed config isconfig-check's job (BDL-068.19-1). Notes, changed paths, scenarios and the commit range arrive as DATA, so the decision runs withoutbd, without git and without a repository. See the Review Brief SPEC. Classified as thereview-brieffeature node.source_derivation/ — package: the three AST derivations BDL-067 wrote as tests, lifted into production by BDL-068 S1.1 (
calls,source_tree,call_graph,termination,branches,body_shapes; the package__init__re-exports the public surface). It answers who else writes this, who else calls this and how many branches does this have and how many ways does it end from the SOURCE rather than from a hand-maintained list, and it answers over a SHAPE rather than a spelling: a reader is a body that lists a directory AND parses YAML — matched over six listing verbs and six loaders, because a detector asking forglob("*.yml")andyaml.safe_loadby name was measured walking past five bodies that read the same directory.callables_that_reach(root, seed)is a least fixed point over the tree;call_sites_in(source, reaching, command=…, marker=…, resolving_in=…)reads one command's branches, the branch each call sits in, and whether the marker call is still reachable after it;writers_that_build(root, key=…, commit_point=…)finds the bodies that create rather than patch. Nothing here is bound to a particular question — the commit point, the payload key, the marker and the module terminator names resolve through are parameters. It imports no other beadloom package and consumes onlyastandpathlib. Ceilings are stated in the component DOC and in each module: a name is not a resolved import, the branch reading is syntactic, an unresolvable terminator reads as continuing, and a writer that hands its commit to a helper is seen as the helper instead.work_item_routing.py — the work-item types, their flows and the documents each writes, DERIVED from the composed
/task-initcommand rather than restated here (BDL-068 S1.5). A work item's type is a claim about how far the change ranges, and BDL-067 was routedbug, wrote one BRIEF, passed one approval gate and became 28 beads — re-deriving its axes afterwards showed four graph nodes. Reading the command's own routing table means a project layer that adds a type or moves one between flows changes the check by the same act, and the command cannot state a route the check does not police.Routing.shared_kindsis the third face of the same computation — the kinds EVERY route writes, taken as an intersection rather than a difference — andbeadloom wavesspends it: a document no route can avoid is one every bead of a work item writes and no bead's code owns. It also reports the line the explore step is stated on and the line the type decision is taken on, which is what makes "the decision cannot be reached before the step" checkable on the artifact rather than asserted about it. The join lives here for the reasondoc_shape.pystates:onboardinganddoc_syncare peer domains. A routing table is found throughdoc_sync.tables.table_blocks— the one component that decides where a markdown table begins — so this is the third reader of that boundary and not a third parser of it (BDL-UX #259, after #213 and #244). Every block whose own header row leads withTypeandFlowis a routing table, and the routes are their union, because a project layer states its own. A row a table states and the derivation cannot read as a route is named inRouting.noteswith its line number rather than dropped. Classified as thework-item-routingcomponent node.declared_scope.py — which work item a commit belongs to, and the scope its
## Axessection declares (BDL-068 S1.6). The joinscope-checkcannot make for itself: the graph index that says which node owns a path, git for the paths and for the branch, and the planning corpus that says which folders are work items. The work item is found by the BRANCH, and it has to be — the pre-commit hook runs before the commit message is finalised, so the[KEY]prefix is not readable at the moment the commit is judged. A folder matches when its name is one of the branch's/-separated segments, over the populationplanning_documents()returns, so a project configuring its owndoc_quality.pathsis judged over its own corpus.trunk_ref()prefersorigin/<trunk>over the local one andpaths_changed_sinceusesref...HEAD, both for one measured reason: with a localmaintwo commits behind the remote,--since mainreported another work item's LANDED change as this branch's and--since origin/maindid not. No branch, no work item, no index, no section and no answer from git are five different reasons to have checked nothing, and each is reported as itself — and sincebeadloom-0mdo.32each reaches the pre-commit hook, which reads the command as2>/dev/nulland so used to see a run that could attribute nothing and a run that found nothing outside as the same empty string.VERDICT_MARKERmarks the verdict line in porcelain output for exactly that split. Classified as thedeclared-scopecomponent node.impact/ — package:
beadloom impact <path|symbol>(BDL-068 S1.2), the feature an adopter runs oversource_derivation/. It answers four questions from the SOURCE — who else commits through the sink this target reaches, who else calls what it defines, how many branches each of its commands has and how many ways each ends — plus the boundary from the graph, which says when a change leaves a bounded context. The seed is derived, never given. BDL-068 S1.3 measured ataf26750dthat the same derivations report 2 writers and 4 branches ofinitunder one seed and 0 and 3 under another, on one tree: three is the number BDL-067 carried for nine review passes, and a command taking the commit point as an argument would ship that as a clean, confident, wrong answer. So the rulereaches-an-effect-sinkderives it — a name the target reaches TRANSITIVELY whose OWN body performs a declared effect directly (serialises-yaml, orreads-a-yaml-directoryas a conjunction) — and the answer names the seed, the rule and the effect.PUTS_BYTES_ON_DISKis deliberately not a declared effect: measured at the same commit it does not contain this product's own commit point, andopenalso reads, so it is not a sound predicate alone. A target no rule finds a sink for is reported as UNRESOLVED rather than answered over an empty set, and what the derivation could not read travels with the answer under twelve named kinds (unreadable-target,target-outside-the-sweep,sweep-narrower-than-the-project,no-seed,no-graph-index,unparsed-module,call-through-a-variable,dynamic-dispatch,unresolved-terminator-name,name-defined-more-than-once,no-node-for-path,node-owns-unread-files). A named node's unread surface is on its row (BDL-UX #284):unread_ownershiplists, per node the answer names, the files that node owns by the most-specific-wins rule and this derivation did not read, and the## Axessection writes the count and the first file in the row'sOwns unreadcolumn. One epic ruledonboardingout as blast radius because it surfaced as a caller, while the fix lived in that node's.md.txttemplates. How wide it swept is a claim the answer can withdraw (BDL-068.15): the walk up from the target does not require__init__.py, because requiring one stopped the sweep at the first subpackage of a PEP 420 namespace tree and reported a caller one directory across asnone found.— resolved, empty and wrong, on every adopter tree and on none of this repository's own, where every package carries the file. It stops instead below asrc-named directory, below one carryingpyproject.toml, and never above the project root;callers.resolvedis a predicate over whether the swept root holds the target rather than the literalTrue; and a sweep narrower than the project's source root says so, carrying both paths. An unresolved axis still prints the sites it did find, under the caveat rather than instead of it. The branch axis is read from the callers' seats as well as the target's: scoped to the target's own file it wrotebootstrap_project: 3 branch(es)whileinit, named one row above as a caller, had the four branches this project got wrong for nine review passes, and every count now carriesTHE_TARGET_SEATorTHE_CALLER_SEAT. It is NOT a graph walk: a node whose axes live entirely inside it still produces an answer, and the graph supplies the boundary and nothing else. Eight modules by responsibility —seeds,axes,boundary,unresolved,unread_ownership,answer,renderandsection— with human,--jsonand--sectionoutput from one computation;sectionrenders the## Axessection a work item's document carries, using the grammardoc_sync.axes_sectiondeclares rather than a second one. See the Impact SPEC. Classified as theimpactfeature node.typed_surface.py — the files a project declares type-checked, and the three sentences a check over them has (BDL-068 S4, BDL-UX #231).
declared_typed_surface(project_root)resolves[tool.mypy]'spackages,modulesandfilesagainstmypy_pathinto the roots a type check covers, andTypedSurface.partition(paths)holds staged paths against them. The surface is DERIVED rather than listed becausebeadloom-mr2l.82listed it in the pre-commit hook template, the mypy configuration then moved and the template did not.[[tool.mypy.overrides]]is outside the read by construction: an override changes which findings are reported, never which files are in the surface.SurfacePartition.describe()has three sentences and not two —NOT CHECKEDwith its reason when no surface could be derived,NOTHING TO CHECKwhen the surface exists and the commit staged nothing inside it, and otherwise a count of the files actually handed to the checker — because a check whose population is empty reading as a check that passed is the phantom this epic is named for. What could not be resolved travels with the answer: a package resolving to no path, a glob matching nothing or several roots, a declaredexclude(not applied, because mypy does not apply it to files named on the command line, which is how the hook invokes it), and amypy.iniorsetup.cfgbeside thepyproject.toml. The declaration is read without a TOML parser for the reasonrooms.pystates about the packaging metadata. Measured on this repository: the hook used to handmypyevery staged.pyundersrc/ortests/, which is 970 errors in 90 files, and over the 24 commits offeatures/BDL-068it warned on 4 of the 7 that staged Python — all 4 false. Surfaced bybeadloom typed-surfaceand consumed by both pre-commit hook templates. Classified as thetyped-surfacecomponent node.rooms.py — the rooms a verdict can be taken in (BDL-068 S3.2).
take_census(project_root)answers three things at once: the room this process is in, derived from the platform and the interpreter rather than typed by a caller; the rooms the project DECLARES, read from theProgramming Language :: Python :: X.Yclassifiers and from every job of every.github/workflows/*.ymlwith its matrix expanded; and which of those the run did not enter. One rule decides the last: a run enters a leg only when every dimension of that leg is comparable and equal, so a runner label naming no platform and a dimension this run cannot describe both resolve to NOT ENTERED — a comparison that cannot be made must never manufacture coverage.requires-pythonis kept as a FLOOR and never counted upward, because enumerating>=3.10needs a hardcoded newest Python, and the packaging metadata is read without a TOML parser so the answer does not differ between 3.10 and 3.13 — a room-dependent answer from the module whose subject is rooms. What could not be derived travels with the answer: a workflow that does not parse, a job with noruns-on, an unresolvableruns-onexpression, a matrix usinginclude/exclude, andpython-version: [3.10]left unquoted, which reaches a reader as the number 3.1. Surfaced bybeadloom rooms, printed under everybeadloom civerdict and carried on the MCPcomplete_beadverdict;mutation_scope.describe_roomcomposes its room sentence here so both surfaces print one sentence. Since BDL-068 S6 the census carries anextrasdimension: what this run has comes from the analysed project's own distribution as the running interpreter holds it (Provides-Extraand theextra ==markers onRequires-Dist), and what a leg installs comes from the install step its job declares, compared on what an environment SATISFIES rather than on what somebody typed — a leg installingdev,languages,tui,watch,graphqlalso satisfiesall. It is the dimension BDL-UX #236 was filed on: measured at6c4d0a9, one code base at one commit gave 0 mypy errors under.[all,dev]and 82 under.[dev]. An interpreter holding no distribution of that name adds no dimension at all, because a value spellingunknownwould read as a difference in the environment when what happened is that nothing looked. Since BDL-068 S6 it also carries alocaledimension, and it is the CODEC in force rather than the name somebody spelled —codecs.lookup(locale.getpreferredencoding(False)), the same derivationci.yml's own anti-vacuity step makes, with a leg's declared name resolved through its codeset. It is the dimension BDL-UX #248 and #249 were filed on: thetests-localelegs used to resolve to "this run cannot describe the dimensionlocale" while the process genuinely was running under an ASCII codec, anden_US.ISO-8859-1— the nameci.ymlpublishes — is not a locale macOS has, so a developer reproducing that leg ran theCroom a second time under the other room's name. A locale the environment asked for and did not get adds a second dimension,locale_asked, present only then, so the room line every verdict carries readslocale ascii (asked for en_US.ISO-8859-1, which did not apply here)and no leg is reported as entered on a name that resolved elsewhere. Naming the room does not make a verdict stronger — it makes it answerable.WORKFLOW_DIRandload_jobs(path)are public because a second derivation reads the same declaration for a different question —gate_coverage.pybelow — and one reader means one wording for "this workflow could not be parsed" rather than two that can drift.typed_extras_of_job(job)is public for the same reason and answers a genuinely different question fromextras_satisfied_by: a room CENSUS asks which extras an environment satisfies, so two identical environments are not reported as two rooms, while a room BUILDER asks what to type into an install command. Both parse a leg's install step through that one function.leg_installs(project_root)is the builder's read of it, anddeclared_extra_names(project_root)enumerates what a leg spelling--all-extrasnames and does not list, read without a TOML parser for the reason above and therefore a lower bound.installed_extras(project_root, search_path=...)reads another environment's installed metadata rather than this process's, which is what lets a clean room record the extras of the interpreter its own verdict is taken under (BDL-UX #256). Classified as theverdict-roomcomponent node.gate_step.py — the shape one gate step reports in (
GateStep, itsstatusproperty,Finding) and the one line the Gate prints about it (gate_step_line). Lifted out ofgate.pybybeadloom-rqma.8and re-exported from it, so none of the forty-five modules that import these names frombeadloom.application.gatechanged. The move was forced rather than chosen: a leg lifted into a module of its own cannot name the type it returns while that type lives in the module that imports it. The shape is what every leg shares and the composition is not, so the shape is what moved. A number a leg's summary states about its findings is the length ofGateStep.findings— thereadme-pairleg counted a second population and printed0 finding(s)in a run whosefindingsheld one.gate_declarations.py — what an unusable opt-in declaration costs a leg, for the two legs that are opt-in (
issue-logreadsissue_log:,readme-pairreadsdocument_pairs:).unusable_phraserenders the; N entr(ies) declared, M unusable: <where> (<why>)clause,unusable_declaration_stepandundetermined_declaration_stepbuild the two steps that say nothing ran, andrefusal_findingprojects one refusal onto the shared finding shape. One home, because the two legs answering did this project declare one? separately is the defect that produced BDL-UX #270 and its twin. What tellsdeclared nonefromdeclared badlyis a count and not an adverb: a skip reworded to possibly nothing was declared would be the same defect in softer words. It reads no config and decides no verdict —doc-sync/components/config-declarationsreads, this renders, the leg composes.gate_document_pairs.py — the
readme-pairleg itself:step_readme_pair, its line, and the projection of what it found. The first leg lifted out ofgate.pyunderbeadloom-oew7, and the reason is measured rather than asserted —gate.pywent 1362 → 1553 → 1624 → 1733 lines while a deferral to lift per-leg rendering out of it stood, so the debt that deferral called bounded grew every time the file was touched. It shipped the count correction with the move:N finding(s)was taken fromPairReport.findings, 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 the SPEC shipped in the same cycle documented the1 finding(s)the code did not produce. The step builds one list, carries it and counts it, so the number and the findings beside it are the same expression (beadloom-rqma.8).gate_coverage.py — the verifications a project's pipeline declares that no step of a gate run performed (BDL-068 S6, BDL-UX #247).
beadloom ciruns reindex, lint, sync-check, docs-audit, docs-quality, doc-spaces, scope-check, config-check and doctor, and it does not run the test suite; the division is reasonable and every step it does run is named, but until this module the run never said the suite was not among them — whileCLAUDE.mdcalls the pre-push hook "the fullbeadloom ci". 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.derive_gate_coverage(project_root, performed=...)answers it from two declarations and no sentence: what this run performed comes from its ownGateStepnames plus anything the caller ran beside it, so a suite step added to the gate later removes the line by the same act; what the project verifies comes from.github/workflows/*.ymlthrough the samerooms.load_jobsreader the room census uses, so one file has one reader and one wording for "this workflow could not be parsed".DUTIESrecognisespytest,ruff/flake8/pylintandmypy/pyrightbehind any runner prefix, and it deliberately does not bind the style duty to the step namelint: the gate's ownlintstep checks the architecture boundaries, not the source style, which is why this repository's block names three and not one — the test suite, the style linter and the type checker. A pipeline verifying under a name the vocabulary does not hold is told the population is empty with the vocabulary named, never that nothing is left to run. Printed under everybeadloom civerdict beside the room lines, carried asnot_runin--format jsonand on the MCPcomplete_beadpayload, which passesperformed_elsewhere=("tests",)when it runs the suite itself so one run cannot report the suite as not run while that run ran it. It is not a step: sameok, same exit code, same findings. Classified as thegate-coveragecomponent node.gate_ownership.py — which bead claimed now owns each finding of a gate run, and the verdict when none does (BDL-068 S6, offered by
beadloom-0mdo.76). The branch carried a red Gate across two waves of 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 it rather than discount it.derive_gate_ownership(project_root, findings=..., tracker=...)answers it from the index and from the tracker: a finding reaches a node through its ownnodefield, then through source ownership of a location path (impact.boundary, the same most-specific-wins ownershipscope-checkuses), then through the documented node of a doc path, since half this gate's findings are about documents and no source prefix owns one. Everything is read through the loader's output rather than by opening a graph file by name, which is whybeadloom-0mdo.80could replaceservices.ymlwith 100 per-node files without a Gate step noticing. The claim comes from the beads the tracker reportsin_progressNOW, parsed by the wave planner's ownresolve_scopeso the gate and the plan cannot disagree about what a bead said; the work item's## Axeslose because they answerscope-check's question and name the work item every agent on the branch shares, and a wave's plan loses because it names beads that have not started and beads whose wave is over. Three verdicts, because they are three facts:ownednames the beads,unownedsays a node was derived and no claim covers it — the interesting case, and precisely what had to be said by hand — andunattributedsays no node could be derived at all. A tracker that cannot answer, and a project with no index, are areasonon the whole report rather than a page ofunowned, and a claimed bead whose own declaration cannot be read is reported beside the verdicts because it qualifies everyunownedin the run. It is not a step: sameok, same exit code, same findings, and the tracker is asked only when the run produced a finding. Printed under everybeadloom civerdict beside the room and coverage lines, carried asownershipin--format json, as::notice::lines in--format githuband asownerson the MCPcomplete_beadFAIL payload. Classified as thegate-ownershipcomponent node.status.py — the read-side of the
beadloom statuscommand (BDL-059 S4; moved down fromservices/cli.py).gather_status(conn, project_root)reads the index/coverage/health/trend counts (its not-fresh count includesmissingpairs: a pair whose file is gone is not one less thing to worry about) plus per-kind breakdown out of the SQLite index into a frozenStatusDatavalue;compute_context_metrics(conn, nodes_count, symbols_count)builds each node's context bundle and returns the average/largest bundle token sizes and total indexed symbols. The CLIstatuscommand keeps only presentation (Rich or JSON rendering) — this module owns the queries.
API
Module src/beadloom/application/reindex/ (package; public surface re-exported from __init__):
ReindexResult— dataclass with counts (includingtest_files_indexedandtest_files_unplaced),nothing_changedflag,errors, andwarningsreindex(project_root, *, docs_dir=None)->ReindexResult— full reindex with sync baseline preservationincremental_reindex(project_root, *, docs_dir=None)->ReindexResult— incremental reindex with parser fingerprint and graph YAML change detectionresolve_scan_paths(project_root)->list[str]— resolves source scan directories from config (defined ininfrastructure/scan_paths.py; re-exported here for backward-compatible import paths)
Module src/beadloom/application/doctor.py:
Severity— enum:OK,INFO,WARNING,ERRORCheck— dataclass:name,severity,descriptionrun_checks(conn, *, project_root=None)->list[Check]— runs DB validation checks plus optional agent instructions freshness check whenproject_rootis provided
Module src/beadloom/application/status.py:
StatusData— frozen dataclass: version/last-reindex, node/edge/doc/chunk/symbol counts, stale/isolated/empty-summary counts, coverage, per-kind breakdown, trends, andcontext_metricsgather_status(conn, project_root)->StatusData— read the full status payload (counts, coverage, health, trends, context metrics) from the indexcompute_context_metrics(conn, nodes_count, symbols_count)->dict— average/largest context-bundle token sizes (+ owning ref_id) and total indexed symbols
Module src/beadloom/application/debt_report/ (package; public surface re-exported from __init__):
DebtReport— frozen dataclass:debt_score(0-100),severity,categories,top_offenders,trend,layer_populations,test_populationload_debt_weights(project_root)->DebtWeightscollect_debt_data(conn, project_root, weights=None)->DebtDatacompute_debt_score(data, weights=None)->DebtReportcompute_debt_trend(conn, current_report, project_root, weights=None)->DebtTrend | Noneformat_debt_report(report)->str— the Rich report; the layer populations, the test population and the offenders' ref ids and reasons are escaped before Rich reads them as markup, so a declared pattern such as__tests__/**/*.[jt]sprints as written (beadloom-2mj3.19)format_debt_json(report, category=None)->dict[str, Any]
Module src/beadloom/application/watcher.py:
DEFAULT_DEBOUNCE_MS— debounce constant (500ms)WatchEvent— frozen dataclass:files_changed,is_graph_change,reindex_typewatch(project_root, debounce_ms=DEFAULT_DEBOUNCE_MS, callback=None)— monitors project files viawatchfiles
Module src/beadloom/application/site.py:
SiteResult— frozen dataclass:out_dir,written(sorted tuple of every written path)MermaidValidationError— raised when a generated page fails the Mermaid guard (carriespage+issues)generate_site(conn, out_dir, *, project_root, federated=None, now_ts=None)->SiteResult— deterministic VitePress tree generator; never writes into the sourcedocs/; guards every emitted diagram. Emits the About homeindex.mdfromREADME.md(fallback: architecture overview), the architecture overview atarchitecture.md, a RU Aboutru/index.mdfromREADME.ru.md(skipped when absent; both link to each other via the in-page/↔/ru/cross-link), and adocs/index.mdDocumentation overview = intro + per-section named-members descriptions, no link wall (BDL-046 BEAD-11).now_tsis the injected ISO-8601 timestamp for the metrics-history point recorded this run (deterministic in tests; defaults to the current UTC instant in production — the only wall-clock read, landing solely in the append-only history store, never in the diffed output)
Module src/beadloom/application/site_mermaid_guard.py:
MermaidIssue— frozen dataclass:kind(reserved-id/charset/c4-rel-undeclared),messagevalidate_mermaid(text)->list[MermaidIssue]— targeted structural guard for flowchart reserved-id/charset + C4 Rel integrity (extensible, deterministic)
Module src/beadloom/application/site_dashboard/ (package; public surface re-exported from __init__):
build_dashboard_data(conn, *, project_root, federated=None)->dict— deterministic dashboard data (lint/debt/docs/doctor + optional federated rollup + critical-firstalerts+ threshold-coloredstatus_cards+trendstime-series + prioritizedrecommendations); honest by construction (reuses each gate's code path; trends are exactly the recorded points)render_dashboard_md(data)->str— renderdashboard.mdfrom the data dict: the page title + a short intro + the<ClientOnly>block mounting the banner + status cards (<AlertBanner/>/<StatusCards/>) and the committed ECharts widgets (<HealthGauges/>/<CategoryChart/>/<TrendCharts/>/<Recommendations/>, theme-registered, readingdashboard.data.json). No per-metric text dump, no<noscript>fallback — the widgets are the single presentation surface (data honesty lives indashboard.data.json)serialize_dashboard_data(data)->str— deterministic JSON (sorted keys) fordashboard.data.json
Module src/beadloom/application/site_metrics_history.py:
MetricsPoint— frozen dataclass:ts,lint_violations,debt_score,coverage_pct,sync_pct,nodes,edges,symbolshistory_path(project_root)->Path—.beadloom/metrics_history.jsonappend_metrics_point(project_root, point)— append/overwrite-by-ts and persist (injected ts; idempotent per ts)read_history(project_root)->list[MetricsPoint]— the recorded series sorted byts(only real points, no fabrication)backfill_structural_history(conn, project_root)— seed structural counts fromgraph_snapshots(idempotent; never clobbers a recorded point)
Module src/beadloom/application/site_landscape.py:
build_landscape_data(conn=None, *, federated=None)->dict— deterministic landscape-map data (scope/nodes/edges); federated when afederateartifact is given, else a single-repo contract map from the local graph (produces/consumesedges reconciled bycontract_keyintoContracts, classified to aContractVerdict; one edge per producer→consumer)render_landscape_md(data, *, pages=None)->str— renderlandscape.mdas a Mermaid diagram (verdict-labelled edges,classDefhealth overlay, clickable nodes); never hand-drawn.pagesis aref_id → existing page URLmap: a node emits aclickONLY when it has a real generated page, so the map never links to a dead URL (a node whose kind has no page directory — e.g. asitenode or a foreign federated repo — renders without a click)existing_page_urls(conn)->dict[str, str]— map every node that has a generated page (kinds with an output directory:service/domain/feature) to its absolute page URL (/<dir>/<ref>); fed torender_landscape_md(pages=…)so landscape clicks resolve to real pages
Module src/beadloom/application/landscape_view.py:
build_landscape_view_data(conn, *, pages=None)->dict— deterministic, renderer-agnostic interactive-landscape data (schema_version/scope/nodes/edges/contracts); contracts reconciled viagraph.contracts.reconcile_contractsfrom each edge'sextra.contractblob, carrying verdict/routing/producers/consumers/missing+ the GraphQL typedfields(S2) or AMQPbodyJSON-Schema (S3); empty surface → undeclared (no fabrication).pagesgives a node a non-emptyurlonly when a real page existsserialize_landscape_view(data)->str— byte-stable JSON (sort_keys, 2-space, trailing newline)render_landscape_view_md(data)->str— render the primarylandscape.md: title + intro +<ClientOnly><LandscapeMap></ClientOnly>mount + a static JS-off count summary + a link to thelandscape-diagramMermaid fallback (pure function ofdata)
Module src/beadloom/application/site_pages.py:
NodeRow/NodePage— frozen dataclasses for a graph node and its rendered pageload_nodes(conn)->list[NodeRow];render_all_pages(conn)-> sortedlist[NodePage]
Module src/beadloom/application/site_nav.py:
human_label(ref_id)->str— title-cased, hyphen→space label (context-oracle→Context Oracle)render_architecture_group(conn)->str— the collapsed,part_of-nested Architecture sidebar group (human labels; self-edge-safe roots; "Architecture overview" →/architecture)render_documentation_group(project_root)->str— the Documentation sidebar group mirroring thedocs/tree (nested, collapsible;/docs/-rooted leaf links)render_documentation_group_from_dir(docs_dir, *, collapsed)->str— the Documentation group from a docs dir with an explicitcollapsedflag (expanded on the site)render_nav()->str— the top-nav JS array, intentionally"[]"(BDL-046; theme keeps appearance toggle + local search)render_sidebar(conn, *, docs_root, has_getting_started)->str— the full ordered, link-safe sidebar (About / Getting Started / flat Dashboard / Architecture / flat Landscape map / expanded Documentation)render_nav_config(conn, project_root)->str— the full deterministic.vitepress/config.generated.mjsmodule: exports onlynav(empty) + the single sharedsidebar. VitePresslocaleswas dropped (BDL-046 BEAD-11), so there is nonavRu/sidebarRu/render_sidebar_ru
Module src/beadloom/application/site_about.py:
render_about(readme_text, *, published_doc_slugs, repo_url, cross_link_routes=None)->str— pure, deterministic README→About transform: rebases doc links to/docs/<slug>; rewritesREADME.md/README.ru.mdcross-links to the route incross_link_routes(the in-page bilingual toggle/↔/ru/) or drops them when no map is given; rewrites other internal targets to absolute GitHub URLs; leaves absolute URLs/badges/anchors and code-span/fenced links untouched (EN/, RU/ru/front-door page)
Module src/beadloom/application/site_published.py:
BADGE_START/BADGE_END— stable markers delimiting the injected badge regionPublishedDoc— frozen dataclass:doc_path,status,reason,synced_at,ref_id,coverage_pctbuild_published_docs(conn, *, project_root)->list[PublishedDoc]— per-doc validation inputs fromcheck_sync(same source assync-check); a doc with no doc-code pair isuntrackedand rendered as a neutral📘 referencebadge (no coverage % line)inject_badge(prose, badge_body)->str— marker-delimited badge prefix; re-injection overwrites only the badge regionrender_published_doc(doc, prose)->str— badged Markdown (badge + authored prose as-is)publish_docs(conn, out_dir, *, project_root)->list[Path]— copydocs/**intoout_dir/docs/…with badges (plus a generateddocs/index.mdlanding page so the/docs/nav target resolves); never mutates the source
Module src/beadloom/application/gate.py:
GateStep— dataclass:name,passed,skipped,findings,summary,not_verified;.status->PASS/WARN/FAIL/SKIP.not_verifiedkeepspassedTrue (a project that cannot supply a baseline is not thereby broken) while stopping the step reading greenGateResult— dataclass:steps,room,coverage,ownership;.ok(all steps passed),.findings(all findings across steps). The last three are qualifications the verdict carries and not steps: which rooms it is true of, which verifications it is a verdict about, and who owns what it found. None of them has a status and none moves the exit code;Noneon any of them means nothing derived it, so a surface that was not told makes no claimrun_ci_gate(project_root, *, fail_on, hub_exports, no_reindex, performed_elsewhere=(), tracker=None)->GateResult— composes reindex → lint → sync-check → docs audit → docs-quality → issue-log → readme-pair → doc-spaces → scope-check → config-check → doctor → (optional) federate; never short-circuits.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; without it the run makes no ownership claim at all rather than reporting every finding as owned by nobodygate_step_line(step)->str— the one line the Gate prints about a step,[STATUS] name: summary. It lives beside the dataclass rather than in the renderer because two commands quote it and one of them is not the Gate:inittells the adopter whatbeadloom ciwill say about the graph it has just judged, and it saidlint - <summary>, a shape nothing prints (BDL-067.14). A line that pre-empts another command's output is produced by that command's own formatter, or it drifts the first time either is rewordedlint_step(project_root)->GateStep— the gate'slint --strictstep, and the only step that is public.beadloom initcalls it over the graph it has just written and exits 1 when it does not pass, so the command cannot report success over a graph that fails the rules the same command wrote (BDL-067.2). Shared rather than restated: the defect being closed, BDL-UX #192, is two halves of one command disagreeing about whether a tree is green, and a second copy ofpassed = not result.has_errorsat the init site is how they would disagree againRULES_CONFIG_ERROR— thelintstep's summary whenrules.ymlcould not be loaded at all. Public because that step is the one case where a finding'sruleis the step's own name rather than a rule's, so a caller rendering findings has to branch on it:beadloom initprints the loader's complaint there and rule names everywhere else (BDL-067.6)- The sync-check step carries the pair's
ref_idas the finding'snodefield, which the linter's findings have carried since BDL-067.14and these named only in English prose. It is what letsgate_ownershipattribute a stale doc — the finding this branch's two-wave red was made of — to a bead without parsing a sentence. It fails onstaleANDmissingpairs (BLOCKING_STATUSES), emits warning findings forunverifiedpairs and for a declared surface smaller than the committed ledger records, and its summary names what it could not check instead of printing a bare pair count (BDL-UX #174/#175). Since BDL-069 its failing line counts stale PAIRS rather than documents, eachdoc-stalefinding names its pair (— pair <doc> <-> <code>), and its remediation is chosen bydoc_sync.engine.attestation_clears: a reason re-attesting cannot clear, such asmissing_modules, is told what does clear it instead of being told to runsync-update(BDL-UX #282) - The doctor step summary counts the CHECKS that ran, by severity —
run_checksreturns one entry per FINDING, so the oldlen(checks)counted problems and ROSE from 20 to 21 while a declared doc was being deleted. The word clean is printed only when every check is OK
Testing
Tests: tests/integration/application/reindex/ and tests/unit/application/reindex/ (bound to reindex since BDL-074 F2; see the reindex SPEC), tests/integration/application/doctor/test_doctor.py, tests/integration/application/doctor/test_doctor_drift.py, tests/integration/application/doctor/test_doctor_instructions.py, tests/unit/application/watcher/test_watcher.py, tests/integration/application/debt_report/test_debt_report.py, tests/unit/application/debt_report/test_debt_integration.py, tests/integration/application/debt_report/test_the_debt_report_reads_the_test_binding.py, tests/integration/application/mutation_scope/change/test_a_change_states_the_one_unplaced_count.py, tests/integration/application/mutation_scope/change/test_a_change_selects_by_placement_and_by_tag.py, tests/integration/application/mutation_scope/acceptance/test_a_step_file_is_read_for_the_scenarios_it_loads.py, tests/integration/application/debt_report/test_an_adopter_scores_what_it_scored_before.py, tests/test_gate.py, tests/test_gate_finding_owner.py, tests/test_site_generator.py, tests/test_site_dashboard.py, tests/integration/application/test_site_landscape.py, tests/test_site_published_docs.py, tests/integration/application/test_site_nav.py