✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (
beadloom)Validation by Beadloom
doc_sync— same source assync-check.
Beadloom Architecture
📘 reference — overview/deep-dive, not tied to a single code symbol. It aligns with (does not replace) the generated
/architectureC4 overview on the published portal.
Beadloom turns Architecture as Code into Architectural Intelligence — structured, queryable knowledge about your system that humans and AI agents consume in <20ms.
System Design
The system is organized into six DDD domain packages, an application (use-case orchestration) layer, and two interface layers:
Domains:
- Context Oracle (
context_oracle/) — BFS graph traversal, context bundle assembly, code indexing, two-tier caching, FTS5 search,whyimpact analysis - Doc Sync (
doc_sync/) — doc↔code synchronization tracking, stale detection, symbol-level hashing, docs audit, document shape and writing-standard quality checks - Graph (
graph/) — YAML graph loader, diff engine, rule engine, import resolver (eleven languages,_EXTENSION_LOADERS), architecture linter, C4 diagram emitter, federation - Onboarding (
onboarding/) — project bootstrap, doc generation/polishing, architecture-aware presets, AGENTS.md / IDE-rules generation, config sync, one skip policy every reader of.beadloom/_graph/applies before it looks at a graph file (graph_files.py), and the agentic-flow composer (flow_config.py,composer.py,role_composer.py,role_adapters.py,flow_manifest.py,flow_suppression.py), which assembles every flow artifact from CORE + architecture + stack + the project layer in.beadloom/flow/ - Infrastructure (
infrastructure/) — domain-agnostic SQLite database layer, health metrics, git-activity tracking, and the configuration readers for where source (scan_paths) and documentation (docs_dir,doc_roots) live - AI Agents (
ai_agents/) — governed AI-agent harnesses that ship inside the wheel; hosts the deterministic, seam-isolated AI tech-writer (ai_agents/ai_techwriter/, run viapython -m beadloom.ai_agents.ai_techwriter). A leaf consumer: it may readapplication/context_oracle/graph/doc_syncAPIs but must never be imported by the core domains or services (enforced by thecore-no-import-ai-agents/application-no-import-ai-agentsforbid_importrules).
Application (use-case orchestration) (application/) — composes the domains into end-to-end use cases. It owns:
reindex/— the full + incremental reindex pipeline (drop → recreate → reload graph/docs/code/sync; SHA-256file_indexfor incremental runs), a cohesion-split packagedoctor.py— graph + data integrity checksstatus.py— the read-side ofbeadloom status(index/coverage/health/trend counts + context-bundle metrics)debt_report/— architecture-debt aggregation, scoring, trend tracking, CI gating, a cohesion-split packagewatcher.py— file watcher for auto-reindex on changegate.py— the unifiedbeadloom cigate (reindex → lint → sync-check → docs audit → docs-quality → doc-spaces → config-check → doctor → optional federate)guards/— the flow-guard primitive behindbeadloom guard(BDL-061 S1): a verdict per named guard, theguards:block of.beadloom/flow.yml, one invocation boundary, and the firing record--livenessreadswaves/— the wave decision behindbeadloom waves(BDL-061 S6): resolve each bead's declared node scope, decide from the graph which beads may run at once with one named reason per serialised pair, and check the plan-time precondition of each of the seven media every wave shares whatever its width, and hold each bead's declaration against the## Axessection its work item recorded, and build the clean room each bead measures in — derived from that bead and created rather than entered — a cohesion-split packagereview_brief/— the reviewer's input behindbeadloom review-brief(BDL-061 S6): assemble the assignment, the declared scope, the specification documents, the bound scenarios and the changed files, and withhold the bead's own comments until a verdict is recorded- the VitePress site generators —
site.py(orchestrator),site_pages.py,site_nav.py,site_about.py,site_dashboard/(a cohesion-split package),site_landscape.py,site_published.py,site_mermaid_guard.py,site_metrics_history.py
Interface layers:
- Services (
services/) — CLI (services/cli.pyis a thin Click registration shell; the command implementations live in theservices/commands/package, one cohesive module per command group) and MCP Server (services/mcp_server.py, stdio server with 18 tools for AI agents — 14 graph read/write tools + four BDL-048 process-tools). Both call into the application layer and Context Oracle; the CLI never reaches past those layers. - TUI (
tui/) — interactive terminal architecture workstation (Textual): dashboard, explorer, doc-status screens.
A layers rule in .beadloom/_graph/rules.yml enforces the direction services / tui → application → domains (the interface layers depend inward; domains never depend on the application or service layers).
Node Kinds
The graph distinguishes the kinds of node it tracks:
| Kind | Doc | Annotation | Description |
|---|---|---|---|
service | services/<name>.md | # beadloom:service=<id> | An interface/process boundary (CLI, MCP server, TUI) |
domain | domains/<name>/README.md | # beadloom:domain=<id> | A DDD domain package |
feature | features/<name>/SPEC.md | # beadloom:feature=<id> | A user-facing capability inside a domain |
component | <name>/DOC.md | # beadloom:component=<id> | An internal/infra building block — the mirror of a feature for code that is not user-facing |
entity / adr | — | — | Domain entities and architecture decisions |
The component kind (BDL-051) and the module-coverage lint (promoted to severity: error) together close the no-shadow-code gap: every src module with at least one symbol must be a tracked node (feature or component, or covered by a node's source — including a directory source like tui/) or named on a small, visible exempt: list in rules.yml. A new untracked module therefore fails beadloom lint --strict / beadloom ci.
Specification
Data Flow
The application/reindex/ orchestrator drives the indexing pipeline, calling each domain in order; the resulting SQLite index is then read back by Context Oracle for sub-20ms context bundles.
YAML Graph Files (.beadloom/_graph/*.yml)
↓
application/reindex/ (orchestrates the pipeline below)
│
├─ graph/loader.py → SQLite (nodes, edges, rules)
├─ graph/import_resolver.py → SQLite (code_imports)
├─ doc_sync/doc_indexer.py → SQLite (docs, chunks, search_index)
├─ context_oracle/code_indexer.py → SQLite (code_symbols)
└─ (writes file_index, health_snapshots)
↓
context_oracle/builder.py ← BFS traversal → context bundle (JSON)
↓ ↕ L1 memory / L2 SQLite cache
services/cli.py / services/mcp_server.py / tui/ → user / AI agentSQLite Schema
The database is stored in .beadloom/beadloom.db and uses WAL mode for concurrent access.
Core tables (9):
| Table | Key columns | Description |
|---|---|---|
nodes | ref_id (PK), kind, summary, source, extra | Graph nodes (domain, feature, service, entity, adr) |
edges | src_ref_id, dst_ref_id, kind (composite PK), extra | Graph edges (part_of, depends_on, uses, implements, touches_entity, touches_code) |
docs | id (PK), path (UNIQUE), kind, ref_id (FK→nodes), hash, metadata | Document index |
chunks | id (PK), doc_id (FK→docs), chunk_index, heading, section, content, node_ref_id | Document chunks (max 2000 chars) |
code_symbols | id (PK), file_path, symbol_name, kind, line_start, line_end, annotations, file_hash | Code symbols (function, class, type, route, component) |
sync_state | id (PK), doc_path, code_path, ref_id (FK→nodes), code_hash_at_sync, doc_hash_at_sync, doc_hash_at_last_edit, synced_at, status, symbols_hash, file_symbols_hash, baseline_source | Doc↔code sync state. Four verdicts (ok, stale, missing, unverified); baseline_source records where the row's baseline came from — index_build, carried or attested — and is carried verbatim across a reindex, never promoted. symbols_hash is the whole NODE's symbol surface, file_symbols_hash this pair's OWN code file: only the second makes a pair stale, and a pair whose sibling moved is unverified/sibling_symbols_changed rather than a stale verdict nobody can discharge (BDL-UX #182) |
declared_docs | declared_path (PK), doc_path, ref_id (FK→nodes) | Every doc path a node DECLARES in its docs: list, whether or not the file is on disk. A cache of the committed graph YAML, refreshed on reindex, so a declaration outlives the file it names |
reference_state | doc_path (PK), watches, aggregate_hash, status | Surface-drift baseline for reference docs that opt in with <!-- beadloom:watches=... -->. Kept apart from sync_state so the symbol-pair logic is untouched |
meta | key (PK), value | Index metadata (key-value) |
Infrastructure tables (8):
| Table | Key columns | Description |
|---|---|---|
health_snapshots | id (PK), taken_at, nodes_count, edges_count, docs_count, coverage_pct, stale_count, isolated_count, extra | Trend tracking across reindexes |
file_index | path (PK), hash (SHA-256), kind (graph/doc/code), indexed_at | Incremental reindex support |
bundle_cache | cache_key (PK), bundle_json, etag, graph_mtime, docs_mtime, created_at | L2 persistent context cache |
search_index | ref_id, kind, summary, content | FTS5 virtual table for full-text search |
code_imports | id (PK), file_path, line_number, import_path, resolved_ref_id, file_hash | Import relationships between files |
rules | id (PK), name (UNIQUE), description, rule_type (free-form TEXT; the loader owns the vocabulary — BDL-061 S4 dropped the CHECK that restated it and broke every existing DB on a new rule type), rule_json, enabled | Architecture rules from rules.yml |
graph_snapshots | id (PK), label, created_at, nodes_json, edges_json | Point-in-time architecture graph captures for drift detection |
foreign_edges | src_ref_id, dst_ref_id, kind, extra, lifecycle, contract_key | Declared cross-repository edges. Separate from edges because a foreign endpoint cannot satisfy the FK to local nodes; beadloom export unions them into the federation artifact |
Test tables (3, BDL-074 C1): test files are indexed apart from code, so they never become code symbols, module coverage or code ownership.
| Table | Key columns | Description |
|---|---|---|
test_files | path (PK), kind, ref_id, placement, test_count, file_hash | Every test file the recorded layout reads, the node it binds to and how: placement is mirror, override or beside_code when bound, and unowned, unplaced or other_kind when not |
test_imports | id (PK), file_path, line_number, import_path, resolved_ref_id | A test file's imports, in the shape of code_imports, read by test_import_boundary |
test_overrides | ref_id, prefix (composite PK) | The tests: path prefixes a node declares in its graph YAML |
BFS Algorithm
Context Oracle uses BFS with edge prioritization:
| Priority | Edge type | Description |
|---|---|---|
| 1 | part_of | Component is part of |
| 2 | touches_entity | Touches entity |
| 3 | uses / implements | Uses / implements |
| 4 | depends_on | Depends on |
| 5 | touches_code | Touches code |
BFS traverses the graph bidirectionally (outgoing + incoming edges), sorting neighbors by priority.
Default parameters:
depth= 2 — graph traversal depthmax_nodes= 20 — node limit per bundlemax_chunks= 10 — text chunk limit per bundle
Rules Engine
Architecture rules are defined in .beadloom/_graph/rules.yml (schema version 3) and enforce boundaries between graph nodes. The YAML key on each rule selects its type.
Rule types — the 15 authoring keys load_rules dispatches, parsed and evaluated by the graph/rules/ package and orchestrated by graph/linter.py. A rule declares exactly one of them; this repository configures 19 rules across them:
| YAML key | Semantics | Example |
|---|---|---|
deny | Forbid depends_on/import relationships between matched nodes | domain:* → service:* — domains must not depend on services |
require | Require edges from matched nodes to targets | Every domain:* must have a part_of edge to the beadloom service |
forbid | Forbid specific edge patterns between tagged node groups | Nodes tagged ui-layer must not have uses edges to native-layer |
layers | Enforce layered architecture direction | Top-down: services → domains → infrastructure |
forbid_cycles | Detect circular dependencies in the graph | No cycles on uses/depends_on edges |
forbid_import | Control file-level import boundaries | Files in src/beadloom/tui/** must not import beadloom/infrastructure/** — note the two vocabularies: from matches the file path, to the dotted import path with dots → slashes (no source root). A src/-prefixed to matches nothing (BDL-UX #172) |
check | Enforce complexity / coverage limits per node | max_symbols: 180 per domain — counting the symbols a node OWNS, nested nodes excluded (Beadloom's domain-size-limit; see the recalibration note below) |
unregistered_feature_candidate | Report a source directory with enough symbols to deserve a node and no node declaring it (BDL-051) | default severity warn |
module_coverage | Report a source module under source_root that no node tracks (BDL-051) | default severity warn; exempt entries carry a reason |
scenario_coverage | Bind behaviour-bearing nodes to executable Gherkin scenarios, both ways (BDL-061 S4) | default severity warn; for / features / references / non_behavioural — see the BDD guide |
doc_area_coherence | Hold a graph to the source-to-docs placement convention derived from the graph itself (BDL-062) | default severity warn, raised to error here; threshold / min_support — no layout literal appears in the rule |
summary_facts | Check a number stated in a node summary against the fact the project computes (BDL-062) | default severity warn, raised to error here; a fact the project cannot compute is reported unverifiable, never clean |
test_binding | Report a test file bound to no node (files leg) and a node with no bound test file, its own or a part_of descendant's (for leg) (BDL-074) | default severity warn; this repository declares the file leg at error and the node leg at warn, as two rules |
test_import_boundary | forbid_import over the imports of test files, narrowed by of to tests of a matching node (BDL-074) | default severity error; tests/unit/** of a layer-domain node must not import beadloom/infrastructure/** |
scenario_binding | A scenario's @node: tag names the node folder its feature file sits in (BDL-074) | default severity warn, raised to error here; whether the scenario's steps EXECUTE that node is not judged |
Internally each parsed rule carries a
rule_typestring (deny/require/forbid/layer/forbid_import/cardinality/scenario_coverage/doc_area_coherence/summary_facts/test_binding/test_import_boundary/scenario_binding/ …) used by the evaluators; the authoring key inrules.ymlis the column above.The two counts above differ because they count different things: 15 is how many keys the loader accepts, 19 is how many rules this repository declares. Only the second is checked by
docs audit— the fact it is checked against is namedrule_type_countand computesSELECT COUNT(*) FROM rules, which is the instance count, not the type count (BDL-UX #179).
Evaluation:
denyrules are checked against thecode_importstable: resolved import ref_ids are matched against rule patternsrequirerules are checked against theedgestable: nodes matching thefor/frompattern must have the specified edge kind to the target- Node matchers support an optional
excludefield (list of ref_ids) to exempt specific nodes from rule matching unless_edgeexemptions allow otherwise-forbidden imports when a specific edge kind exists between the nodesforbidrules check edge patterns between nodes matching tag selectorslayersrules verify dependency direction across ordered architectural layersforbid_cyclesuses an iterative WHITE/GREY/BLACK colored DFS (ingraph/rules/cycles.py) to find circular dependency paths, reporting each unique cycle onceforbid_importrules query thecode_importstable for forbidden cross-boundary imports; a rule whose glob matches zero candidates anywhere in the index is itself reported (rule_type: rule_liveness,warn) rather than counted clean, andexempt:entries — which baseline a pre-existing crossing and must carryreason+until— are reported the same way once they suppress nothing, or once anuntil:that leads with an ISO date passes while they still suppress something. What the remaining entries excused is counted on every run (violations_suppressed), so0 violationscannot mean0 violations we counted(BDL-061.49)checkrules count symbols/files per node (cardinality) and verify module coverage;module-coverageisseverity: error- Every rule type reports its own inertness, not only
forbid_import: a matcher selecting no node, an edge kind the graph does not have, acheckwith no threshold, asource_rootwith no module. A rule that cannot match is otherwise indistinguishable from one that passed, and both read asN rules evaluated, 0 violations. Severity depends on how much the rule stood down. A partial inertness — one dead glob beside nine live ones, an exemption that excuses nothing, a matcher selecting no node while the rule's other legs still fire — iswarnwhatever the rule declares: it describes the configuration, not the code, anderrorwould turn an adopter's green pipeline red on upgrade. A total stand-down, where the rule could check NONE of its population, carries the severity the project declared. At that point "found nothing wrong" and "never ran" are the same output, and a project that deliberately escalated the rule has had its escalation evaporate exactly when it mattered (BDL-062.9, BDL-UX #195). What that costs an adopter depends on the severity the rule ships:doc-area-coherenceshipswarnand still reportswarn, so nothing changes for anyone, whilegraph-summary-factsshipserrorand will block a project whose summaries state no checkable number — measured on a graphbeadloom init --mode bootstrapproduced, 0 of 3 summaries state one, so that run is reachable rather than exotic.severity: warnon the rule is the one-key opt-out (BDL-062.14).module_coverage,scenario_coverageandunregistered_feature_candidatestill report a total stand-down atwarn; all three shipwarn, so only a project that escalated them is affected (BDL-UX #197).lint's summary carriesrules_inertso the advertised rule count cannot over-claim (BDL-UX #172)
Output formats:
- Rich — human-readable with Unicode indicators (✓, ✗, ▲, ▼)
- JSON — structured violations array + summary
- Porcelain — machine-readable, one TAB-separated line per violation
CLI: beadloom lint [--strict] [--format rich|json|porcelain] [--no-reindex]
The --strict flag exits with code 1 on error-severity violations (for CI/CD). Rules support error and warn severity levels.
domain-size-limit: 200 → 280 (BDL-059 S3) → 290 (BDL-060 S4) → 180 (BDL-UX #144). The first two moves were threshold recalibrations under the OLD metric, wheremax_symbolscounted every file under a node's path prefix — so an in-domain split changed nothing and the honest option was to raise the bar for legitimately large bounded contexts.The last move is different in kind: the metric changed meaning.
max_symbolsnow counts the symbols a node OWNS — files under its source minus files owned by a more specific node — which is what makes the rule's own remedy work: carving a subpackage into its own node genuinely relieves the parent. Under the new metric this repo's largest owner isapplicationat 150 (it was 284 under prefix counting), so 290 could never fire again; 180 restores the signal with ~20% headroom.One consequence is stated openly: this measures a node's OWN code, not its bounded context's total size. A domain carved into many features owns little and will not trip the rule however large its subtree grows. That is a genuinely different signal, tracked as BDL-UX #158 rather than smuggled into this threshold. The limit stays a
warnsignal for re-scoping, never a target.
Node Tags
Nodes can be assigned tags for use in rule matching:
# In services.yml
nodes:
- ref_id: my-feature
kind: feature
tags: [ui-layer, presentation]Tags are arbitrary strings. Rules reference them via { tag: <tag-name> } selectors in forbid_edge and layer rules.
Cache Architecture
Context bundles use a two-tier cache to achieve <20ms response times:
L1 — In-memory (ContextCache):
- Key:
(ref_id, depth, max_nodes, max_chunks) - Invalidation: mtime comparison against
.beadloom/_graph/and docs directories - Cleared on reindex
L2 — SQLite (SqliteCache):
- Table:
bundle_cache - Key:
"<ref_id>:<depth>:<max_nodes>:<max_chunks>" - Survives MCP server restarts
- Invalidation: mtime-based, same as L1
ETag validation:
- Format:
"sha256:<first-16-hex-chars>"of sorted bundle JSON - Returned on cache hit with
cached: trueandunchanged_sincetimestamp - Clients skip re-fetching if ETag is unchanged
Incremental Reindex
The file_index table tracks SHA-256 hashes of all indexed files (graph YAML, docs, source code).
Process:
- Scan relevant files across graph, docs, and source directories
- Compute SHA-256 hash per file
- Compare with stored
file_index.hash - Only re-parse files with changed hashes; skip unchanged
- Update
file_indexwith new hash and timestamp - Return
ReindexResultwithnothing_changedflag
When nothing changed, the CLI displays current DB counts instead of "0 indexed".
Health Snapshots
Each reindex captures a health snapshot:
| Metric | Description |
|---|---|
nodes_count | Total graph nodes |
edges_count | Total graph edges |
docs_count | Total indexed documents |
coverage_pct | % of nodes with linked docs |
stale_count | Stale sync_state records |
isolated_count | Nodes with zero edges |
Trends: compared against the previous snapshot, displayed as ▲ +8%, ▼ +2, etc. Arrows are inverted for "bad increase" metrics (stale, isolated). Snapshots persist across reindexes.
Architecture Snapshots
beadloom snapshot manages point-in-time captures of the architecture graph for historical comparison.
Commands:
beadloom snapshot save [--name NAME]— save current graph statebeadloom snapshot list— list saved snapshotsbeadloom snapshot compare [SNAP_ID]— compare current graph with a snapshot
Snapshots are stored in SQLite and enable architecture drift detection across releases.
Agent Prime
beadloom prime outputs a compact project context (target: ≤2000 tokens) for AI agent session initialization.
Sections:
- Project metadata (name, version)
- Architecture summary (node counts by kind, symbol count)
- Health metrics (stale docs, lint violations, last reindex)
- Architecture rules (from rules.yml)
- Domain list (all domain nodes with summaries)
- Stale docs (doc/code path pairs)
- Lint violations (evaluated without reindex)
- Key CLI commands (reference table)
- Agent instructions (workflow guidance)
Output formats: Markdown (default), JSON.
Graceful degradation: works without DB (static-only mode with warning).
CI Gate
application/gate.py powers beadloom ci — the unified gate that composes the existing checkers into one verdict with a single exit code: reindex → lint --strict → sync-check → docs audit → docs-quality → doc-spaces → config-check → doctor → (optional) federate landscape gate. Every step's honest result is printed (PASS / WARN / FAIL / SKIP) — never a green that silently skipped a step. --format rich|json|github applies uniformly; --hub <export> arms the cross-service landscape gate. The same gate runs as the pre-push Beadloom Gate hook (install-hooks --pre-push) and in CI (.github/workflows/ci.yml).
WARN is a fourth outcome, not a softer pass: a step sets not_verified when it ran, found nothing wrong, and could not establish that there was nothing to find. It does not change the exit code, so no adopter's green project turns red on upgrade, and it does not print the word that would claim more than the run knows.
Freshness rests on git, not on the database
.beadloom/beadloom.db is a derived cache: git-ignored, per-machine, rebuilt by any reindex --full, and absent on every fresh CI checkout. A baseline kept only there is destroyed by the act that most needs it — a rebuilt index adopted the current tree as its own baseline, so sync-check reported every pair fresh by construction (BDL-UX #175). Two baselines therefore live outside it, both committed:
- Freshness — git. Each
sync_staterow records itsbaseline_source. One fabricated at index-build time that would otherwise readokis corroborated againstHEADthrough a singlegit status --porcelain -z -uall; where git cannot answer, the pair readsunverifiedrather than fresh. Acarriedbaseline is copied verbatim across a reindex and never promoted, so a fabricated one does not become earned by being copied. - Surface size —
.beadloom/sync-surface.json. Committed, and written only by the deliberatesync-check --record-surface. A later run whose declared surface fell says so with both numbers. A ratchet only works if lowering it is a deliberate act left in the history, so no ordinary run rewrites the ledger — a check that silently re-records the number it is checking against re-attests without evidence (BDL-UX #163).
Two consequences for anyone scripting around the index. Deleting the database and rebuilding is no longer a way to reach a green sync-check, and the retired instruction to verify on a clean database — right for lint, vacuous for sync-check — must not come back. What a green still does not prove is stated rather than left to be found: the git leg compares the working tree against HEAD, so it cannot judge whether a long-committed document still describes its code. --since <ref> answers that, and an incremental reindex keeps the stronger accumulated index baseline where one exists.
Agentic Flow Composer
The onboarding domain composes the packaged multi-agent dev flow from a declaration in .beadloom/flow.yml plus the adopting repository's own fragments under .beadloom/flow/:
.beadloom/flow.yml ──load──▶ flow_config.py
(FlowConfig: tools, architecture, stack,
quality, language, suppressions)
│
▼
composer.py compose(kind, name, config=, project_root=)
│ 1. CORE fragment (stack-neutral)
│ 2. SHARED core fragments (core:_writing,
│ core:_rooms, core:_landing, core:_tracker)
│ 3. ONE architecture overlay (ddd|fsd)
│ 4. sorted stack overlays
│ 5. .beadloom/flow/<kind>/<name>.md ← the project
│ + the overlays.suppress notice
▼
┌───────────────────────┼────────────────────────┐
▼ ▼ ▼
role_composer.py + agentic_flow_setup.py agentic_flow_setup.py
role_adapters.py composed_command() composed_claude_md()
│ │ │
▼ ▼ ▼
.claude/agents/* .claude/commands/* .claude/CLAUDE.md
.cursor/agents/*
└───────── every write recorded in ─────────────┘
.beadloom/flow-manifest.jsonflow_config.py—FlowConfig(frozen) +resolve_flow_config(flag →flow.yml→ default) +detect_stack; strict validation. Supported: toolsclaude/cursor; architectureddd/fsd(exactly one); stackpython/fastapi/javascript/typescript/vuejs.languageis validated for shape, not against a closed list;overlays.suppressis validated throughflow_suppression.composer.py—compose(kind, name, *, config, project_root)for the four kindsroles/commands/claude/docs(BDL-061 S4b moved the document skeletons out ofdoc_generator.py's string literals intotemplates/docs/;docsis the one kind composed withcarries_suppressions=False, because a suppression stands down a rule addressed to an agent and a generated README has none). Deterministic: the same inputs always yield the same bytes, with no dependence on the clock or on ambient state. That property is what licensesconfig-checkto compare against a composition rather than against stored bytes. The CORE fragments live atonboarding/templates/roles/core/<role>.md.txt,onboarding/templates/agentic_flow/commands/<cmd>.md.txtandonboarding/templates/agentic_flow/CLAUDE.md.txt; the overlays live underonboarding/templates/{roles,commands,claude}/{architecture/<arch>,stack/<stack>}/. The commands andCLAUDE.mdkept their vendored location as the CORE and gained an overlay root beside it, because moving them would have churned the whole scaffold for no signal.role_composer.py—compose_role(role, *, architecture, stack, language, suppressions, project_root), the roles-shaped door ontocompose; FSD at parity with DDD.SHARED_ROLE_FRAGMENTS = ("_writing", "_rooms", "_landing", "_tracker")composes four shared texts into all five roles as labelledcore:<name>layers, so a rule addressed to several roles has one text rather than five copies that drift the moment one is edited, and each is language-selectable like every other layer._writingis the writing standard (BDL-061 S4), carried by every role because the roles that produce intent documents had none;_roomsis the statement that a measurement is true of the room it was taken in (BDL-068 S3.2);_landingis what the merge slot grants and what it does not, for every role that lands a commit in a shared tree;_trackeris which population each ofbd's answers covers, for every role that reads one (both BDL-068 S5).role_adapters.py—generate_adapters(config, project_root)writes the per-tool adapter set(s).beadloom setup-agentic-flow --tool/--architecture/--stackis the CLI entrypoint.flow_manifest.py— the sha256 of every composed write, which is what lets a later run tellstale(recomposable) fromhand_edited(reported, never rewritten) frommissingfromunverified..beadloom/flow-manifest.jsonis generated state and belongs in git.flow_suppression.py— a declared stand-down of a core rule (rule+reason+until, all mandatory), rendered as a visible notice into every composed artifact. Expiry is aconfig-checkfinding rather than a byte, so the composition stays a function of its inputs.config_sync.py— compares each artifact against its composition, maps the manifest state onto a severity, names the project layer in effect and reports suppression liveness.
The core CLAUDE.md measures 377 lines (down from 440), with each removed line mapped to a replacement in a stack overlay or in §0 CRITICAL RULES. The project layer is what makes that shrinkage possible: a project's own rules have a home that survives an upgrade instead of being appended to a drift-guarded shipped file.
The AI tech-writer harness (ai_agents/ai_techwriter/) is the runtime half of the flow: a PR-triggered, symbol-scoped, bounded-parallel doc-refresh harness — see the ai_agents domain README + the ai-techwriter feature SPEC.
Invariants
- All
ref_idvalues are unique within the graph - Edges reference only existing nodes (FK with ON DELETE CASCADE)
- A document is linked to at most one node via
ref_id - On full reindex all tables except
health_snapshotsare recreated (drop + create) - WAL mode is enabled on every connection open
- Foreign keys are enabled per-connection
Constraints
- Code indexer parses every extension in
_EXTENSION_LOADERSvia tree-sitter:.py,.ts,.tsx,.js,.jsx,.go,.rs,.kt,.kts,.java,.swift,.m,.mm,.c,.h,.cpp,.hpp. Reindex change detection reads the same set - Import analysis covers Python, TypeScript, JavaScript, Go, Rust, Kotlin, Java, Swift, Objective-C, C and C++ over 17 file extensions — the keys of
context_oracle.code_indexer._EXTENSION_LOADERS.supported_extensions()narrows that set to the grammars actually installed, so a missing optional tree-sitter package removes an extension rather than failing the walk. The count of parsed languages is deliberately not written as a digit here:language_countin the audit's fact vocabulary means the languages this project is WRITTEN in (1), so a digit beside the wordlanguagesis read as a claim about that and reported stale - Documentation root is configurable via
docs_dirin.beadloom/config.yml(default:docs/) - Documentation SPACES (TO-BE / AS-IS / WORKING) — their roots, kinds, intent documents and the WORKING freshness exemption — are configurable via
doc_rootsin.beadloom/config.yml - Source scan paths are configurable via
scan_pathsin.beadloom/config.yml(default:src,lib,app) - Graph is read only from
.beadloom/_graph/*.yml - Rules are read from
.beadloom/_graph/rules.yml - The 15 authoring keys
load_rulesdispatches:deny,require,forbid,layers,forbid_cycles,forbid_import,check,unregistered_feature_candidate,module_coverage,scenario_coverage,doc_area_coherence,summary_facts,test_binding,test_import_boundary,scenario_binding. A rule declares exactly one of them, andgraph.rules.loader.AUTHORING_KEYSis the single definition of the set ai_agentsis a leaf consumer — never imported by core domains/services (forbid_importenforced)- Maximum chunk size: 2000 characters
- Levenshtein suggestions: maximum 5, distance threshold = max(len/2, 3)
Configuration
.beadloom/config.yml:
| Key | Default | Description |
|---|---|---|
languages | all supported | File extensions to parse (e.g. [".py", ".ts"]) |
scan_paths | ["src", "lib", "app"] | Source directories to scan |
docs_dir | docs/ | Documentation root directory |
doc_roots | see the Document Kinds guide | Per-space roots, kinds, intent documents and the WORKING freshness exemption |
tests | see Getting Started | The test layout: roots (default tests, test, spec, __tests__, each where it exists, and only those that exist are named), kind folders, patterns by framework (a file name, or the end of a path such as __tests__/**), build-tool test trees, beside_code |
sync.hook_mode | warn | Pre-commit hook mode: warn or block |