Skip to content

✅ fresh

last synced 2026-09-29T21:12:19.681221+00:00 · coverage 96% (application)

Validation by Beadloom doc_sync — same source as sync-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 ci enforcement 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 by test_index in 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 → record tests: 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 its exempt: entries since BDL-070 B4, because the architecture view reads its rule from the rules table 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, through infrastructure.node_source.NodeSource, the rule git activity and docs polish call too. Until BDL-069 beadloom-rqma.4, enrichment matched by string prefix and gave src/ledger/ the routes of src/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 from file_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. Backfills symbols_indexed from the live DB (total code_symbols count) so the result reports the true symbol total, not just the per-run delta (mirroring the #88 nodes/edges backfill). Since BDL-074 C1 test_index records 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's tests: declaration or, since BDL-074 G2, its place inside a node's source, and rebuilds nodes.extra["tests"] from that binding in the four-key shape; the heuristic _store_test_mappings step is removed. Its placement_counts() delegates to infrastructure.repository.count_test_files_by_placement since BDL-074 C2, because ctx and the debt report state the same counts and neither may import the reindex. Since BDL-074 F1 kind_counts() reads the other_kind files by recorded kind (count_other_kind_test_files), and the reindex Tests: line names each kind with its count (72 acceptance step, 103 self-check) where it used to say bound 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's tests: block (roots, kind folders, patterns by framework, build-tool test trees, beside_code; since beadloom-2mj3.15 the default roots are tests/, test/ and spec/, joined by a top-level __tests__/ in beadloom-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 as meta.test_layout with the roots that exist and, as absent_roots, the ones that do not, and an unusable key or a node's tests: 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 as nothing_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 — stale and missing, since "No stale sync entries" over a deleted document is the false green the missing verdict exists to end (BDL-UX #174) — source coverage gaps) plus an optional "Agent Instructions" check when project_root is 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 vs get_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 string pytest. All four read OK on this repository by coincidence, and a TypeScript adopter with a correct CLAUDE.md was told their stack claim was missing keywords and their test framework was not pytest. They now read the project through onboarding.scanner.project_facts — its declared version, its own src/ packages, its flow.yml stack, and whatever its manifests declare — and a fact the project does not declare is INFO … not verified rather 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 by detect_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_docs reports three outcomes, not two (BDL-062 .4): a node with no document is WARNING, a node whose graph YAML records docs_absent: <reason> is INFO with 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 is WARNING again, 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 into nodes.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 carries extra["tests"] with no bound test file, and while any test file is unplaced the count is withheld (0) and DebtData.test_population / DebtReport.test_population say 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 since beadloom-2mj3.15 the 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 by beadloom-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 as test_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_populations carry how much of its edge set each declared layer rule judged, in the shared population_phrase wording, because the error and warning counts are counts OVER a population and this collector is one of the two surfaces that call evaluate_all without ever building a LintResult (BDL-070 A4). The Rich report prints them as counted over: ... under Rule Violations and the JSON carries them under layer_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 using watchfiles. Graph changes trigger full reindex; other changes trigger incremental. WatchEvent frozen dataclass captures per-event metadata. DEFAULT_DEBOUNCE_MS constant (500ms).

  • site.py — generate_site(conn, out_dir, *, project_root, federated=None, now_ts=None) is the docs site use-case: it reads the indexed graph read-only and writes a VitePress content tree under out_dir (default site/) — an index.md About home page rendered from the project README.md via site_about.render_about (link-rebased; falls back to the architecture overview body when no README.md), a ru/index.md RU About page from README.ru.md (omitted when absent; both About pages get the in-page bilingual cross-link / ↔ /ru/ via cross_link_routes), an architecture.md architecture page — the interactive Cytoscape+ELK compound graph primary view (public/architecture.data.json, delegated to architecture_view.py) plus the Mermaid counts/C4/health overview demoted to the architecture-diagram.md fallback (the body that used to live at index.md, BDL-046; the Mermaid graph was unreadable so the interactive view is now primary, BDL-060 S4 ext), a docs/index.md Documentation 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 to site_pages.py), the metrics dashboard (dashboard.md + dashboard.data.json, delegated to the site_dashboard/ package), the 🌟 landscape map — the interactive Cytoscape+ELK primary view (landscape.md + public/landscape.data.json, delegated to landscape_view.py) plus the Mermaid fallback (landscape-diagram.md, delegated to site_landscape.py), and .vitepress/config.generated.mjs (nav/sidebar). Before building the dashboard it backfills structural trend history from graph_snapshots and records this run's honest metrics point (site_metrics_history.append_metrics_point) so the emitted trend series includes "now"; now_ts is 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 source docs/. Returns a frozen SiteResult listing every written path. Reuses graph/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 raises MermaidValidationError and 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 sorted NodePages; each page has summary, source, public symbols, a Relationships section, linked hand-written docs (rooted at /docs/ so they resolve to the published copy under site/docs/…), and an embedded scoped C4/Mermaid diagram. The Relationships section renders OUTGOING part_of/depends_on/uses edges as Markdown links to other node pages, then INCOMING relationships: Used by — the sorted, deduped union of incoming uses+depends_on consumers (who consumes this node; no separate "Depended on by" section) — and Parts — incoming part_of child 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.mjs module exporting only nav + sidebar (BDL-046 BEAD-11 dropped VitePress locales — its global /x↔/ru/x mapping translated the whole menu and 404'd off /ru/ — so there is a single shared EN sidebar and no navRu/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 is collapsed: true and a part_of-nested tree (service root → domains → features) with human-readable labels via human_label (context-oracle → Context Oracle), roots being nodes with no real part_of parent (a root part_of root self-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 is collapsed: false (expanded) and mirrors the docs/ directory tree (render_documentation_group_from_dir(docs_dir, *, collapsed)) as a nested, collapsible structure (each subdir a group, each .md a 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): a docs/<x>.md link whose slug is in published_doc_slugs → the extension-less site link /docs/<x>; a README.md/README.ru.md cross-link → if its lowercased basename is in cross_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 and render_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 via graph/linter.lint), debt (debt_report.compute_debt_score + compute_debt_trend, serialized via format_debt_json), docs (coverage % + sync_state freshness % + stale pair count, read-only), doctor (doctor.run_checks pass/fail summary), and an optional federated rollup (per-service edge-verdict health + contract-verdict counts) reusing the federate output verbatim. It also emits trends — the recorded time-series from site_metrics_history.read_history (sorted by ts; 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.json the CI harness emits (absent/empty/corrupt → an empty-but-present section, never an error): runs[] sorted by ts with per-run + cumulative docs-refreshed and input/output token spend (ONLY real recorded runs — same no-interpolation contract as trends), totals, and a cost_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_TOKENS rate, never a hard cost (rendered by the AiTechwriterActivity widget) — and recommendations — a prioritized, actionable list built from the EXISTING gate data (one item per lint violation, BREAKING/DRIFT contract risks from the --federated artifact, stale docs from sync_state, and worst-debt nodes from debt_report top 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 emits alerts — 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 reads N stale pair(s) since BDL-069 beadloom-yn6i, because its count is one sync_state row 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 — and status_cards — one threshold-colored card per metric group ({group, label, status, value, detail} with status ∈ ok/warn/error, the severity computed deterministically in Python so the front-end only paints the color). render_dashboard_md emits 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 from dashboard.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) and render_landscape_md(data, *, pages=None) renders a Mermaid diagram from it (never hand-drawn). With a federated.json (the F2 federate hub output) nodes are the satellites and edges are the cross-repo links carrying the hub's ContractVerdict-style verdict verbatim; without it the map is the LOCAL contract graph — _local_landscape reads the repo's own produces/consumes edges, reconciles them by contract_key into graph.contracts.Contracts, classifies each to a ContractVerdict, and renders one edge per producer→consumer coloured by that verdict (Beadloom's own site emits a single beadloom → vitepress-site CONFIRMED edge; a repo with no contracts → an empty map). This is the real contract reality, not the structural depends_on/uses arch (which stays in the C4 overview). Edges are labelled by their verdict; a Mermaid classDef health overlay colours nodes (green = healthy, red = broken, grey = external/expected) and broken edges get a red linkStyle. Clicks are page-aware: a node emits click <id> "/<dir>/<ref>" ONLY when pages (from existing_page_urls(conn)) has a real generated page for it — a node with no page (a site node, 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 named graph becomes n_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 interactive landscape_view.py map.

  • 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 SAME graph.contracts.reconcile_contracts path the gate/report use — never a re-implemented surface — by reconstructing the contract-bearing edge dicts from each edge's extra.contract blob (mirroring the satellite-export path), so each contract carries its ContractVerdict, protocol routing (AMQP exchange/routing_key/message_type, or GraphQL schema), producer↔consumer endpoints, the named missing break paths, and the DEEP field surface: the GraphQL typed Tier-A fields (exposed/referenced, S2) OR the AMQP body JSON-Schema (body.exposed/referenced, S3). Honest degradation: a contract with no declared surface carries an EMPTY fields/body block (the view renders undeclared) — never a fabricated field. Nodes carry kind/group/health (worst incident verdict) + a page url (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) renders landscape.md — the title + intro + the <ClientOnly><LandscapeMap></ClientOnly> mount + a static count summary (JS-off fallback) + a link to the landscape-diagram Mermaid 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 under site/public/landscape.data.json (VitePress copies public/ to the dist root) for the runtime withBase("/landscape.data.json") fetch.

  • architecture_view.py — the interactive LOCAL architecture graph (BDL-060 S4 ext): the primary architecture.md view, replacing the unreadable Mermaid top-level diagram (demoted to the architecture-diagram.md fallback). 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 (the nodes/edges/docs/sync_state/code_symbols tables) — never a re-implemented surface. Each node carries kind/summary/layer (the node's own declared layer tag, minus the conventional layer- prefix)/group/symbol count/doc_status (fresh/stale/none)/published doc_links/page url/its compound parent (the part_of container)/and the beadloom why lists (depends_on/depended_on_by) plus the DECLARED runtime coupling lists (uses/used_by); edges carry depends_on (drawn solid) + part_of (containment → ELK compound parents) + uses (drawn dotted). The uses relation 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 from depends_on and never carries a violation flag — 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 (every cli 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 indexed rules table — the same graph every other read here goes through, so the site needs no second path to rules.yml — and resolves membership through graph.rules.layers, the lookup the rule engine decides on. layer reads the node's OWN tag and layer_rank inherits through part_of, because the card states what a node declares while the layout needs a lane for a feature that declares nothing. The edge violation flag is the RULE's verdict since BDL-070 B4: the module calls graph.rules.layer_edges.flagged_layer_edges, so an edge is red here exactly when beadloom lint reports 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 than depends_on flags nothing here: this picture renders the verdict on dependency arrows, so such a rule is reported by lint and drawn by nothing. Honest degradation: a node with no doc gets EMPTY doc_links (no fabricated link); no declared layer tag → empty layer; a graph whose index carries no layer rule gets no lanes rather than every node in lane 0, and a project that carries layer-* 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); the lint_clean flag is OMITTED entirely when lint_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) renders architecture.md — title + intro + the <ClientOnly><ArchitectureMap></ClientOnly> mount + a static count summary (JS-off fallback) + a link to the architecture-diagram Mermaid 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 under site/public/architecture.data.json for the runtime withBase("/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 of MermaidIssue for the two F4 render bug classes: (1) a flowchart/graph node id that equals a reserved Mermaid keyword or has an illegal charset; (2) a C4 Rel(a, b, …) whose endpoint is not a declared Container/Component/Person/System* node (a Rel to the boundary/root crashes drawRels). An extensible validator registry; deterministic (issues in source order). site.generate_site calls 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.json of MetricsPoints (ts, lint_violations, debt_score, coverage_pct, sync_pct, nodes, edges, symbols). append_metrics_point(project_root, point) records one point per docs site run (the ts is supplied by the caller — never now() inside this module — so tests are deterministic; appending an existing ts overwrites that point so a re-run does not double-count); read_history(project_root) returns the series sorted by ts (only real recorded points, never an interpolated one); backfill_structural_history(conn, project_root) seeds structural counts (nodes/edges/symbols) from the existing graph_snapshots history 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 REAL docs/** tree into out_dir/docs/… preserving structure (the source of truth, rendered as-is) and injects a per-doc validation badge into the COPY only — the source docs/ is NEVER mutated (no AI prose-rewriting; that is the deferred F4.1). A generated docs/index.md landing 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 the doc_sync engine via check_sync — the SAME code path beadloom sync-check runs — so a doc the gate calls stale shows stale on 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 show last synced (the stored sync_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-quality gate step and beadloom docs quality each assembled the run themselves before this, and two assemblies of one report can disagree about what was checked. applicable is stated for all eleven, because a check reported as 0 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 the planning-report component 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 into check_sync; planning_documents / planning_document_globs find the documents the writing-standard checks read (default .claude/development/docs/features/*/*.md, overridable by doc_quality.paths in .beadloom/config.yml); document_section_requirements(project_root) does the same for PLANNING document kinds (BRIEF, RFC, ...), derived from the same composed /templates command; shipped_placeholders derives the placeholder vocabulary from that command's fenced blocks; shipped_decision_sections derives, the same way, the sections the shipped templates put a reason-carrying table under, which is how decision-reason tells a decision table from a measurement table that happens to carry a Reason column (BDL-068 S6, BDL-UX #213). The join lives HERE because onboarding (where the templates are) and doc_sync (where the checks are) are peer domains that must not import each other. Classified as the doc-shape-requirements component 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 epic CONTEXT.md/BRIEF.md Related Files section, because that list is a declaration. The unscoped version was measured first, over 60 epic directories. It attributed the ref status to 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 a CONTEXT.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, and unresolved_reasons now says which of three situations each unresolved epic is in — the document declares no node, the directory carries none of the configured intent_documents, or the one it carries cannot be decoded (reported as intent_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, and relation_checked says 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_reason says which of the two ways its statuses are unknown, epics_unknown_to_tracker names them, and one that declares a node is reported as epic_not_in_tracker — bd close writes only the local database, so an epic leaves .beads/issues.jsonl by ordinary use and the relation would otherwise stop checking it in silence. TrackerRead / read_tracker_export carry the statuses together with the source that answered, because the gate reads the committed export and the command prefers the live bd database. 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 one ACTIVE.md made a kinds: [ACTIVE, SPEC] line covering 39 SPEC.md files report nothing (working_exemption_inert, with working_reach carrying 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 as document_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_excused is a count of sync PAIRS and arrives as an argument from whoever ran check_sync, never recomputed here: one run printed exempt: 0 and 55 WORKING document(s) exempt two lines apart about one tree, and a caller that ran no freshness check passes None so 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_epic read and group the tracker export. It lives HERE because the answer joins the graph, the tracker and infrastructure/doc_roots.py — three readers no single domain owns. Classified as the doc-spaces component 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 through doc_spaces.read_epic_intents — the same declaration join, never a second one — and returns the port type context_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 in context_oracle because 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 close writes only the local database, so the committed export and the live tracker disagree on a branch and a bead status shown inside ctx would 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 a build_context of 8.5 ms, paid on a cold bundle only. Classified as the intent-reader component 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) reports mutation-outside-source, mutation-target-missing and mutation-zero-mutants, all warn, against mutation.targets in .beadloom/flow.yml and scan_paths/languages in .beadloom/config.yml. Surfaced by config-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.py is 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 reports mutation-target-unmeasured, mutation-run-zero-mutants and mutation-counters-missing, all warn. 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 under src/ imports a runner. Since BDL-068 S3.3 report_mutation_score also folds check_mutation_scope over the targets the run is answerable for (filtered by --only), so the three SCOPE findings reach the score and not only config-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 by beadloom 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 by acceptance.py's acceptance_files_by_node from the literal pytest-bdd scenarios()/scenario() paths a step file hands over) and the unplaced test files alone as the runner's fallback (unplaced_tests, which replaced unbound_tests: a self-check or an acceptance step file is never the fallback). Since BDL-074 F1 the plan also carries test_placements and other_kinds, so the Binding: line states the unplaced count ctx and the debt report state (context_oracle.test_binding.describe_unbound, over the recorded layout ChangePlan.test_layout since beadloom-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 (formerly score._is_covered) is the one path-containment rule both the score and the change use. Classified as the mutation-scope component 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.py until BDL-068 S5, whose own docstring already needed an "and" to describe itself; the file moved with git mv and __init__.py re-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 the bd status it comes from), reconcile.py (the reconcile core) and staging.py (what a reconcile may stage, which is never more than the commit already carries). split_table_row/is_separator_cells are 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.py re-exports them for back-compat); bd_status_to_cell(bd_status) is the documented bd-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's beadloom-mr2l.22 onto 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 every features/*/ACTIVE.md), locates the bead-status table's Status column by header index (3- or 4-col), and rewrites only the cells whose state drifts from the injected bd statuses — preserving a richer note when the state already agrees — returning a ReconcileResult for --check vs 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 the active-table component node (its own DOC.md).

  • gate.py — run_ci_gate(project_root, *, fail_on, hub_exports, no_reindex) is the unified CI enforcement gate (the beadloom ci orchestrator). It composes the existing checkers IN ORDER — reindex (unless no_reindex) → lint --strict → sync-check → docs audit → docs-quality (BDL-061 S4b: the five writing-standard checks over the project's planning documents; warn only, 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 CHECKED for a check that read nothing anywhere, NO CHECK READS for a document KIND no content check enters, NOT CLASSIFIED: N table(s), M row(s) for a table decision-reason could not place as a table of decisions (BDL-UX #213 — a Reason column does not make a table a decision table, and a measurement row judged as one is a false positive against honest documentation), and UNREADABLE: N for documents nothing could decode — each sets not_verified, so the step reports WARN rather than PASS → issue-log (BDL-068 S6: the issue log's numbers — duplicate-number, unwritten-claim and unclaimed-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 no issue_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 until beadloom-rqma.7 and now fails with 0 leg(s) run; 1 entr(ies) declared, 1 unusable: issue_log (...), BDL-UX #270 — and not_verified says when the ledger has no floor and two of the three legs entered no number. Its line also states the PARTIAL case since beadloom-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 under document_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 the issue-log reason rather than the docs-quality one: 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 report 0 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 ci exited 0 on all four, measured at HEAD on a foreign two-package project — and each is a finding now whose line carries 1 entr(ies) declared, 1 unusable: document_pairs[0] (...), because what tells declared none from declared badly has to be a count rather than an adverb (beadloom-rqma.7). Both legs read their declaration through doc-sync/components/config-declarations, so the rule about what a misdeclaration costs has one home; a .beadloom/config.yml that 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 — with UNREADABLE: naming each declared path nothing read and NOT COMPARED: counting the pairs whose two files were read and hold no block between them, the second of which sets not_verified so the step reports WARN rather than PASS) → doc-spaces (BDL-061 S5: the TO-BE → AS-IS relation; warn only, a NAMED skip on a project with no TO-BE document, and not_verified when 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.jsonl export rather than a bd subprocess, so the gate gives the same answer in a fresh CI checkout with no tracker installed, and the line names it. The sync-check line states what a WORKING declaration EXCUSED, with the reason it was declared with: the exempt verdict 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 on GateStep.pairs_excused and handed to the doc-spaces step, 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 ## Axes its work item declared; warn only, 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>...HEAD is what the pull request contains → config-check (AgentConfigAsCode, which also carries the mutation-SCOPE findings) → doctor (graph/data integrity; only ERROR-severity checks fail the gate, so advisory WARNING/INFO checks never block — no false gate) → (when hub_exports given) federate --fail-on — into one GateResult whose .ok is True only when every step passed. It ORCHESTRATES existing domain code; it reimplements no checker (the doctor step reuses doctor.run_checks). Honesty invariants: no short-circuit (every step runs and ALL findings are collected even after an earlier failure) and no silent skip (each GateStep records PASS/WARN/FAIL/SKIP, where WARN is 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). The docs-audit line 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 turning 3/9 declared fact(s) verified into 3/8 in 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: git is derived from a .git a git archive export cannot carry, so git 2.49.0 was 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. The lint line carries _population_note beside _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 and 16 of 362 and 362 of 362 read 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 from graph/linter.py) uniformly across all steps, so --format json/github are 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 (the tui-no-direct-infra boundary), so it consumes graph-index data — nodes, edges, symbols, hierarchy — through this facade, which delegates to infrastructure/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 through analyze_git_activity / GitActivity from infrastructure/git_activity.py: repairing that boundary rule's dead to: 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, the guards: block of .beadloom/flow.yml and the request context into a GuardVerdict whose exit code a harness acts on. run_invocation(...) is the one boundary every beadloom guard invocation returns through, so a failure anywhere becomes a recorded verdict rather than a traceback. Checks read the world only through the ports in contract.py — the real bd/git probes live in services/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 with errors='strict', so an undecodable event is refused under every locale rather than only under a UTF-8 one; and paths.py catches Exception around Path.resolve(), so a symlink loop — which raises RuntimeError on 3.10–3.12 and raises nothing on 3.13 — is a stated refusal on every interpreter. firing.py bounds the record it writes (BDL-061.56): the active guard-firings.jsonl rolls over at ACTIVE_FIRINGS_CAP (2000) records into guard-firings.1.jsonl, so --liveness parses 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 input liveness.py reads — so fired_count, never-fired and the last outcome are the same numbers after a rollover as before it. What it costs is per-firing why text older than one generation, and GuardLiveness.carried_count says 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.py reads 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 to PathScope.UNDETERMINED, matches no exclusion and carries the undetermined write set into not_covered instead of reading as a clean pass; and surface.py reports what fraction of the write paths the emitted role adapters grant is named by a registered matcher, deriving the matchers from .claude/settings.json and the population from each adapter's tools: 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 reason typed_surface.py below has three: an empty population is not an unreadable one, and 0 of 0 write path(s) bound read as full coverage in the instrument built to report that class (BDL-068 S4.x, BDL-UX #239). covered is None in both non-fraction states and unresolved / nothing_to_check tell them apart. The report also names the corpus it answered about — the artifacts on disk — because onboarding.role_duties answers 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 redacting KEY=value and 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: error is a target the guard refuses to interpret, which stops that edit at exit 2 and stops nothing else, while unresolved is an inability the guard has about ITSELF — its own code will not import, its flow.yml will 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: a git mv left services/bd_seam without an __init__.py, guard_probes.py:79 imports it to reach the tracker, and Bash, Write and Edit all returned the same ImportError at 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, because Bash belongs on the matcher; the verdict on inability moved instead. Permitting is not passing: the outcome is named on stderr beside PERMITTED_UNGUARDED, is recorded, and does not clear never-fired in --liveness, where is_unanswered is 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 declares refs: <ref_id> at the start of a line in the tracker, the scope expands downward through part_of, and a pair is serialised for exactly one named reason — blocked_by_bead, unresolved_scope, shared_node, shared_file, dependency_edge or override_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, a refs: 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 by beadloom waves, beadloom review-brief and the MCP bead_context tool: 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 dangling refs: header. A human outranks the computation through waves.overrides in .beadloom/flow.yml, each entry carrying a reason and an exit condition (the same exit_condition_deadline the 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 the gate_owner that 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 as room_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_size is the width of one plan and not a claim about solitude, and the not_applicable verdict 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 a WaveEnvironment gathered 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. The focus-document medium 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 reports 0 serialisations truthfully about the wrong population — measured on this project's own S6, where one bead's hunk in docs/domains/application/README.md landed inside another bead's commit, and again where four concurrent beads all wrote into one ACTIVE.md. It is a medium and not a serialisation because it cannot be one: docs.ref_id holds at most one node per document and shared_node fires on any ref intersection first, so a serialisation derived from document ownership adds nothing shared_node does not already produce. The population is DERIVED — Routing.shared_kinds is the intersection of the document kinds every route of the composed /task-init writes, 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. The graph-files medium 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 shape beadloom-0mdo.66 already took at the boundary for issue numbers — which changes every adopter's .beadloom/_graph/ layout and is filed rather than taken. The landing-order precondition is landing.lock_sites(invocations): the command reads the composed flow artifacts an agent is HANDED, the seam's one bd grammar parses them, and this module reports every instruction of bd merge-slot whose call form grants less than it is relied on for -- an acquire with no --holder (one tracker actor for every role, so the holder cannot be told from the claimant), a release with 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 as unknown-form rather 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, and release --holder refuses 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.51 generalised it to every bd subcommand 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: approved is the KEPT rows and nothing else, because a node owning a file the Derived by field ran over reached the approval without anyone deciding it -- six nodes on this repository, two of them carrying rows that say no (BDL-UX #250) -- and a swept node nobody ruled on is swept_no_scope_decision rather than not_derived, which would say the derivation never reached it. The unguarded_axis remedy 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 the bd seam. 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, and rebuild replaces a room whose .beadloom-room.json names 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 --carry flags 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 beside rebuild REPLACES 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 and environment=False is 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 as sync-check exit 2 with stale: 2 against a change that is clean at HEAD (BDL-UX #243). It carries git archive HEAD plus the files the caller NAMES, with no "everything that differs from HEAD" mode, because on a shared tree that set holds the neighbour's work. room_invocation(path) hands back the PYTHONPATH an 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.py creates 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 install dev, languages to 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; and interpreter.extras is then read off the room's own interpreter through installed_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 venv 0.082 s and uv pip install -e 1.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 --parent once printed 0 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 finding running_not_compared. See the Wave Plan SPEC. Classified as the wave-plan feature 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 the changed-outside-scope finding 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: or REVIEW 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 as change_measured: false with a finding, never as an empty change set. Scope is resolved through waves.scope.resolve_scope, the one parser of the refs: 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 of git log <base>..HEAD, and the launch prompt — each either inspected or NAMED as one this command cannot inspect, because 0 withheld was true of bead comments and was read as a claim about the reviewer's knowledge while the account reached three reviewers through ACTIVE.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 in ROLE_NAMES and every shipped command fragment, composed for this project's flow.yml and including its project layer, matched by shape rather than by spelling — so a team that names its own document in .beadloom/flow/roles/review.md moves 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 and 0 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, where features/BDL-068-S2S3 names 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 in declared_scope.py or nowhere). Four channels is what the report states, not what exists. A flow.yml that will not parse costs the documents channel and not the brief: prompts_naming_documents answers None — which is not {} — and the channel reports NOT INSPECTED, because naming a malformed config is config-check's job (BDL-068.19-1). Notes, changed paths, scenarios and the commit range arrive as DATA, so the decision runs without bd, without git and without a repository. See the Review Brief SPEC. Classified as the review-brief feature 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 for glob("*.yml") and yaml.safe_load by 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 only ast and pathlib. 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-init command 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 routed bug, 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_kinds is the third face of the same computation — the kinds EVERY route writes, taken as an intersection rather than a difference — and beadloom waves spends 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 reason doc_shape.py states: onboarding and doc_sync are peer domains. A routing table is found through doc_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 with Type and Flow is 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 in Routing.notes with its line number rather than dropped. Classified as the work-item-routing component node.

  • declared_scope.py — which work item a commit belongs to, and the scope its ## Axes section declares (BDL-068 S1.6). The join scope-check cannot 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 population planning_documents() returns, so a project configuring its own doc_quality.paths is judged over its own corpus. trunk_ref() prefers origin/<trunk> over the local one and paths_changed_since uses ref...HEAD, both for one measured reason: with a local main two commits behind the remote, --since main reported another work item's LANDED change as this branch's and --since origin/main did 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 since beadloom-0mdo.32 each reaches the pre-commit hook, which reads the command as 2>/dev/null and so used to see a run that could attribute nothing and a run that found nothing outside as the same empty string. VERDICT_MARKER marks the verdict line in porcelain output for exactly that split. Classified as the declared-scope component node.

  • impact/ — package: beadloom impact <path|symbol> (BDL-068 S1.2), the feature an adopter runs over source_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 at af26750d that the same derivations report 2 writers and 4 branches of init under 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 rule reaches-an-effect-sink derives it — a name the target reaches TRANSITIVELY whose OWN body performs a declared effect directly (serialises-yaml, or reads-a-yaml-directory as a conjunction) — and the answer names the seed, the rule and the effect. PUTS_BYTES_ON_DISK is deliberately not a declared effect: measured at the same commit it does not contain this product's own commit point, and open also 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_ownership lists, per node the answer names, the files that node owns by the most-specific-wins rule and this derivation did not read, and the ## Axes section writes the count and the first file in the row's Owns unread column. One epic ruled onboarding out as blast radius because it surfaced as a caller, while the fix lived in that node's .md.txt templates. 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 as none 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 a src-named directory, below one carrying pyproject.toml, and never above the project root; callers.resolved is a predicate over whether the swept root holds the target rather than the literal True; 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 wrote bootstrap_project: 3 branch(es) while init, named one row above as a caller, had the four branches this project got wrong for nine review passes, and every count now carries THE_TARGET_SEAT or THE_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, render and section — with human, --json and --section output from one computation; section renders the ## Axes section a work item's document carries, using the grammar doc_sync.axes_section declares rather than a second one. See the Impact SPEC. Classified as the impact feature 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]'s packages, modules and files against mypy_path into the roots a type check covers, and TypedSurface.partition(paths) holds staged paths against them. The surface is DERIVED rather than listed because beadloom-mr2l.82 listed 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 CHECKED with its reason when no surface could be derived, NOTHING TO CHECK when 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 declared exclude (not applied, because mypy does not apply it to files named on the command line, which is how the hook invokes it), and a mypy.ini or setup.cfg beside the pyproject.toml. The declaration is read without a TOML parser for the reason rooms.py states about the packaging metadata. Measured on this repository: the hook used to hand mypy every staged .py under src/ or tests/, which is 970 errors in 90 files, and over the 24 commits of features/BDL-068 it warned on 4 of the 7 that staged Python — all 4 false. Surfaced by beadloom typed-surface and consumed by both pre-commit hook templates. Classified as the typed-surface component 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 the Programming Language :: Python :: X.Y classifiers and from every job of every .github/workflows/*.yml with 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-python is kept as a FLOOR and never counted upward, because enumerating >=3.10 needs 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 no runs-on, an unresolvable runs-on expression, a matrix using include/exclude, and python-version: [3.10] left unquoted, which reaches a reader as the number 3.1. Surfaced by beadloom rooms, printed under every beadloom ci verdict and carried on the MCP complete_bead verdict; mutation_scope.describe_room composes its room sentence here so both surfaces print one sentence. Since BDL-068 S6 the census carries an extras dimension: what this run has comes from the analysed project's own distribution as the running interpreter holds it (Provides-Extra and the extra == markers on Requires-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 installing dev,languages,tui,watch,graphql also satisfies all. It is the dimension BDL-UX #236 was filed on: measured at 6c4d0a9, 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 spelling unknown would read as a difference in the environment when what happened is that nothing looked. Since BDL-068 S6 it also carries a locale dimension, and it is the CODEC in force rather than the name somebody spelled — codecs.lookup(locale.getpreferredencoding(False)), the same derivation ci.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: the tests-locale legs used to resolve to "this run cannot describe the dimension locale" while the process genuinely was running under an ASCII codec, and en_US.ISO-8859-1 — the name ci.yml publishes — is not a locale macOS has, so a developer reproducing that leg ran the C room 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 reads locale 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_DIR and load_jobs(path) are public because a second derivation reads the same declaration for a different question — gate_coverage.py below — 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 from extras_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, and declared_extra_names(project_root) enumerates what a leg spelling --all-extras names 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 the verdict-room component node.

  • gate_step.py — the shape one gate step reports in (GateStep, its status property, Finding) and the one line the Gate prints about it (gate_step_line). Lifted out of gate.py by beadloom-rqma.8 and re-exported from it, so none of the forty-five modules that import these names from beadloom.application.gate changed. 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 of GateStep.findings — the readme-pair leg counted a second population and printed 0 finding(s) in a run whose findings held one.

  • gate_declarations.py — what an unusable opt-in declaration costs a leg, for the two legs that are opt-in (issue-log reads issue_log:, readme-pair reads document_pairs:). unusable_phrase renders the ; N entr(ies) declared, M unusable: <where> (<why>) clause, unusable_declaration_step and undetermined_declaration_step build the two steps that say nothing ran, and refusal_finding projects 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 tells declared none from declared badly is 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-declarations reads, this renders, the leg composes.

  • gate_document_pairs.py — the readme-pair leg itself: step_readme_pair, its line, and the projection of what it found. The first leg lifted out of gate.py under beadloom-oew7, and the reason is measured rather than asserted — gate.py went 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 from PairReport.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 printed 0 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 the 1 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 ci runs 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 — while CLAUDE.md calls the pre-push hook "the full beadloom 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 own GateStep names 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/*.yml through the same rooms.load_jobs reader the room census uses, so one file has one reader and one wording for "this workflow could not be parsed". DUTIES recognises pytest, ruff/flake8/pylint and mypy/pyright behind any runner prefix, and it deliberately does not bind the style duty to the step name lint: the gate's own lint step 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 every beadloom ci verdict beside the room lines, carried as not_run in --format json and on the MCP complete_bead payload, which passes performed_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: same ok, same exit code, same findings. Classified as the gate-coverage component 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 own node field, then through source ownership of a location path (impact.boundary, the same most-specific-wins ownership scope-check uses), 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 why beadloom-0mdo.80 could replace services.yml with 100 per-node files without a Gate step noticing. The claim comes from the beads the tracker reports in_progress NOW, parsed by the wave planner's own resolve_scope so the gate and the plan cannot disagree about what a bead said; the work item's ## Axes lose because they answer scope-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: owned names the beads, unowned says a node was derived and no claim covers it — the interesting case, and precisely what had to be said by hand — and unattributed says no node could be derived at all. A tracker that cannot answer, and a project with no index, are a reason on the whole report rather than a page of unowned, and a claimed bead whose own declaration cannot be read is reported beside the verdicts because it qualifies every unowned in the run. It is not a step: same ok, same exit code, same findings, and the tracker is asked only when the run produced a finding. Printed under every beadloom ci verdict beside the room and coverage lines, carried as ownership in --format json, as ::notice:: lines in --format github and as owners on the MCP complete_bead FAIL payload. Classified as the gate-ownership component node.

  • status.py — the read-side of the beadloom status command (BDL-059 S4; moved down from services/cli.py). gather_status(conn, project_root) reads the index/coverage/health/trend counts (its not-fresh count includes missing pairs: 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 frozen StatusData value; 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 CLI status command 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 (including test_files_indexed and test_files_unplaced), nothing_changed flag, errors, and warnings
  • reindex(project_root, *, docs_dir=None) -> ReindexResult — full reindex with sync baseline preservation
  • incremental_reindex(project_root, *, docs_dir=None) -> ReindexResult — incremental reindex with parser fingerprint and graph YAML change detection
  • resolve_scan_paths(project_root) -> list[str] — resolves source scan directories from config (defined in infrastructure/scan_paths.py; re-exported here for backward-compatible import paths)

Module src/beadloom/application/doctor.py:

  • Severity — enum: OK, INFO, WARNING, ERROR
  • Check — dataclass: name, severity, description
  • run_checks(conn, *, project_root=None) -> list[Check] — runs DB validation checks plus optional agent instructions freshness check when project_root is 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, and context_metrics
  • gather_status(conn, project_root) -> StatusData — read the full status payload (counts, coverage, health, trends, context metrics) from the index
  • compute_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_population
  • load_debt_weights(project_root) -> DebtWeights
  • collect_debt_data(conn, project_root, weights=None) -> DebtData
  • compute_debt_score(data, weights=None) -> DebtReport
  • compute_debt_trend(conn, current_report, project_root, weights=None) -> DebtTrend | None
  • format_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]s prints 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_type
  • watch(project_root, debounce_ms=DEFAULT_DEBOUNCE_MS, callback=None) — monitors project files via watchfiles

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 (carries page + issues)
  • generate_site(conn, out_dir, *, project_root, federated=None, now_ts=None) -> SiteResult — deterministic VitePress tree generator; never writes into the source docs/; guards every emitted diagram. Emits the About home index.md from README.md (fallback: architecture overview), the architecture overview at architecture.md, a RU About ru/index.md from README.ru.md (skipped when absent; both link to each other via the in-page / ↔ /ru/ cross-link), and a docs/index.md Documentation overview = intro + per-section named-members descriptions, no link wall (BDL-046 BEAD-11). now_ts is 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), message
  • validate_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-first alerts + threshold-colored status_cards + trends time-series + prioritized recommendations); honest by construction (reuses each gate's code path; trends are exactly the recorded points)
  • render_dashboard_md(data) -> str — render dashboard.md from 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, reading dashboard.data.json). No per-metric text dump, no <noscript> fallback — the widgets are the single presentation surface (data honesty lives in dashboard.data.json)
  • serialize_dashboard_data(data) -> str — deterministic JSON (sorted keys) for dashboard.data.json

Module src/beadloom/application/site_metrics_history.py:

  • MetricsPoint — frozen dataclass: ts, lint_violations, debt_score, coverage_pct, sync_pct, nodes, edges, symbols
  • history_path(project_root) -> Path — .beadloom/metrics_history.json
  • append_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 by ts (only real points, no fabrication)
  • backfill_structural_history(conn, project_root) — seed structural counts from graph_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 a federate artifact is given, else a single-repo contract map from the local graph (produces/consumes edges reconciled by contract_key into Contracts, classified to a ContractVerdict; one edge per producer→consumer)
  • render_landscape_md(data, *, pages=None) -> str — render landscape.md as a Mermaid diagram (verdict-labelled edges, classDef health overlay, clickable nodes); never hand-drawn. pages is a ref_id → existing page URL map: a node emits a click ONLY 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. a site node 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 to render_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 via graph.contracts.reconcile_contracts from each edge's extra.contract blob, carrying verdict/routing/producers/consumers/missing + the GraphQL typed fields (S2) or AMQP body JSON-Schema (S3); empty surface → undeclared (no fabrication). pages gives a node a non-empty url only when a real page exists
  • serialize_landscape_view(data) -> str — byte-stable JSON (sort_keys, 2-space, trailing newline)
  • render_landscape_view_md(data) -> str — render the primary landscape.md: title + intro + <ClientOnly><LandscapeMap></ClientOnly> mount + a static JS-off count summary + a link to the landscape-diagram Mermaid fallback (pure function of data)

Module src/beadloom/application/site_pages.py:

  • NodeRow / NodePage — frozen dataclasses for a graph node and its rendered page
  • load_nodes(conn) -> list[NodeRow]; render_all_pages(conn) -> sorted list[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 the docs/ 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 explicit collapsed flag (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.mjs module: exports only nav (empty) + the single shared sidebar. VitePress locales was dropped (BDL-046 BEAD-11), so there is no navRu/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>; rewrites README.md/README.ru.md cross-links to the route in cross_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 region
  • PublishedDoc — frozen dataclass: doc_path, status, reason, synced_at, ref_id, coverage_pct
  • build_published_docs(conn, *, project_root) -> list[PublishedDoc] — per-doc validation inputs from check_sync (same source as sync-check); a doc with no doc-code pair is untracked and rendered as a neutral 📘 reference badge (no coverage % line)
  • inject_badge(prose, badge_body) -> str — marker-delimited badge prefix; re-injection overwrites only the badge region
  • render_published_doc(doc, prose) -> str — badged Markdown (badge + authored prose as-is)
  • publish_docs(conn, out_dir, *, project_root) -> list[Path] — copy docs/** into out_dir/docs/… with badges (plus a generated docs/index.md landing 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_verified keeps passed True (a project that cannot supply a baseline is not thereby broken) while stopping the step reading green
  • GateResult — 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; None on any of them means nothing derived it, so a surface that was not told makes no claim
  • run_ci_gate(project_root, *, fail_on, hub_exports, no_reindex, performed_elsewhere=(), tracker=None) -> GateResult — 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. tracker is the read port over the work tracker, supplied by the service that runs the gate because the bd seam 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 nobody
  • gate_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: init tells the adopter what beadloom ci will say about the graph it has just judged, and it said lint - <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 reworded
  • lint_step(project_root) -> GateStep — the gate's lint --strict step, and the only step that is public. beadloom init calls 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 of passed = not result.has_errors at the init site is how they would disagree again
  • RULES_CONFIG_ERROR — the lint step's summary when rules.yml could not be loaded at all. Public because that step is the one case where a finding's rule is the step's own name rather than a rule's, so a caller rendering findings has to branch on it: beadloom init prints the loader's complaint there and rule names everywhere else (BDL-067 .6)
  • The sync-check step carries the pair's ref_id as the finding's node field, which the linter's findings have carried since BDL-067 .14 and these named only in English prose. It is what lets gate_ownership attribute a stale doc — the finding this branch's two-wave red was made of — to a bead without parsing a sentence. It fails on stale AND missing pairs (BLOCKING_STATUSES), emits warning findings for unverified pairs 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, each doc-stale finding names its pair (— pair <doc> <-> <code>), and its remediation is chosen by doc_sync.engine.attestation_clears: a reason re-attesting cannot clear, such as missing_modules, is told what does clear it instead of being told to run sync-update (BDL-UX #282)
  • The doctor step summary counts the CHECKS that ran, by severity — run_checks returns one entry per FINDING, so the old len(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