Component Architecture Audit

Table of Contents

1. Summary

The Component Architecture Audit is the recurring cleanliness check we run on one component at a time. It verifies the component still matches the ground-truth architecture: its shape (simple or composite), its folder morphology, its naming, its tests, its documentation, and its diagrams. One pass covers one component and runs every so often.

The audit does not do large refactorings. A pass fixes only what is mechanical and trivial: folder structure, names, leftover artefacts, stale or missing documentation, and stale ground-truth documents. Everything deeper becomes a recorded finding, so a separate pass can prioritise and refactor it later. The complexity measure feeds that prioritisation.

The audit is one of three checks a component carries. The drift check (Codegen Fix Drift) proves regeneration identity. The code review (Code Review Checklist) proves code quality. This audit proves architecture cleanliness against Component architecture and the applied-MASD documents.

Return to Knowledge.

2. Detail

2.1. When to run it

A component audit runs every so often, and always on these triggers:

  • A component reaches zero codegen drift. Audit it before the task that cleared it closes, not after. Clearing drift says the generated tree matches the generator and nothing more, so closing on that alone declares a component sound on evidence nobody gathered. The close-task skill carries this as its first step.
  • The component's folder layout or codegen templates change.
  • A new sub-component is added to a composite.
  • Any doubt appears about a component's shape or structure.

A sprint row for the work reads: "Component architecture audit: ores.<name>".

2.2. Readiness

A component is auditable at any time. The audit is most valuable when the drift gate is already live for the component, because the generated output is then stable and the audit reads the real tree.

The drift gate is live for these components as of 2026-09-06: ores.refdata, ores.dq, ores.iam, ores.compute. Audit these first. The remaining components follow their drift-baseline stories; the rollout order is in Entity classification and drift baseline across all components. Use the complexity measure (see below) to order the rest of the audit backlog when more than one component is ready.

2.3. How to run one pass

  1. Run the automated checks (next section). Fix nothing yet; collect the findings.
  2. Walk the manual checklist in order. Classify every finding.
  3. Triage: fix mechanical and trivial findings in this pass. Record structural findings as tasks or stories, each with a one-line justification. Do not start a large refactoring.
  4. When the ground-truth documents disagree with the tree (component_architecture.org, the System Model layer pages, the codegen component catalogue), fix the document here. The documents lead; a discrepancy in them is a documentation bug.
  5. Re-run the automated checks. A clean component has no entry left for it in docs_exceptions.txt.

2.4. Automated checks

One tool covers the mechanical checks repo-wide: projects/ores.codegen/validate_docs.py, run through its wrapper:

projects/ores.codegen/validate_docs.sh

The tool walks every projects/ores.* directory and its composite parts, and checks:

  • modeling/component_overview.org presence, frontmatter, and the six required sections on every component overview.
  • A .puml diagram beside every component overview.
  • Top-level name collisions: no ores.<a>.<b> directory next to an ores.<a> component (NAME_COLLISION).

The root modeling/ of a composite is a group-level index (Summary, Sub-components, Entity modules). It is exempt; the parts carry the component docs. Known gaps are recorded in projects/ores.codegen/docs_exceptions.txt with the reason, and the file must stay green on the tree. Deleting an exception line is part of the fix.

2.5. The signed-off folder catalogue

A component tree uses only signed-off folder names. The catalogue is the complete list, with the technical space each folder belongs to. Any folder name outside it is a finding.

Folder Applies to Technical space Meaning
CMakeLists.txt Simple root; composite root; every part root CMake Build definition. A composite root's only job is to add its parts, and projects/CMakeLists.txt adds the component rather than each part.
include/ Simple; every part C++ Public headers, nested one level as include/ores.<full-name>/
src/ Simple; every part C++ Implementation; mirrors the facet dirs of include/
tests/ Simple; every part C++ Catch2 tests; the file name ends in _tests.cpp
modeling/ Simple; parts; composite root Codegen org models Entity org models, component_overview.org, PlantUML sources. Not a CMake target.
api, core, service Composite children C++ Standard part roles
server, client, wrapper, database, quant Composite children C++ Specialist part roles
Domain-area names ores.shell children C++ One module per domain area
venv/ Python tools only Python Local interpreter. Never committed; it is gitignored.

The role of every part name is defined once in the Component architecture sub-component catalogue; this table does not repeat it. Facet dirs inside include/ and src/ (e.g. domain, repository, messaging) follow the per-facet catalogues of Applied MASD and the generated ores.cpp inventory.

Tool components (ores.codegen, ores.compass, ores.sql) keep their own documented trees and their component overviews. The rules above cover the C++ domain components and their parts.

Never tracked, in any component: __pycache__/, *.pyc, .DS_Store, *.orig, *.rej, backup files, build output. A name like stub, crap, or test (singular) at a tree level that has no signed-off meaning is a leftover: delete it, or move its content to the right home. Empty directories serve no purpose once their content is gone; remove them.

2.6. Manual checklist

The automated tool covers the mechanical parts. Walk the rest by hand, in order.

2.6.1. Component shape

The component is exactly one meta-model. A simple component owns its code directly (CMakeLists.txt at the root, include/, src/, tests/, modeling/ inside). A composite component is a parent directory of sub-component dirs, each an independent CMake target. The two shapes are defined in Component architecture. No component is both at once.

The collision check automates the failure mode: a top-level ores.<a>.<b> next to an ores.<a> composite claims a parentage the tree does not have. Resolve it by absorbing the simple component into the composite as a proper sub-component, or by renaming it. Use the sub-component catalogue names, and add the name to the catalogue when the part is a role the catalogue does not yet carry — an unregistered folder name is a finding in its own right, so absorbing a component without registering its role trades one finding for another.

quant arrived this way. It sat as ores.analytics.quant beside the ores.analytics composite, and is now the quant part of it. The move cost nothing at the call sites because a part's CMake target and include prefix are already the fully-qualified name, so only the directory and its registration in projects/CMakeLists.txt changed.

2.6.2. Sub-component structure

Each part of a composite is one of the catalogue roles (api, core, service, and the specialist roles), with the dependency order api to core to service. The parent root holds a CMakeLists.txt that adds its parts and nothing else, and projects/CMakeLists.txt adds the component rather than each part. Each part carries the full simple layout: its own CMakeLists.txt, include/ores.<group>.<part>/, src/, tests/, modeling/. Parts register in their component's CMakeLists.txt in dependency order. Facets sit in the part the facet placement table assigns, and facet dirs appear in the same part in include/ and src/. The Component architecture facet table and the component-only checks of Code Review Checklist are the reference.

2.6.3. Folder morphology

List the tree and compare against the signed-off catalogue above. A folder that is not in it is a finding. Decide one of three outcomes: delete it, move its content into a signed-off folder, or sign it off (agree the meaning, add the row with its technical space). Facet dirs are signed off through the facet catalogues, not through this table. Tool components keep their own trees as documented in their overviews.

2.6.4. Technical space of folders

Every folder belongs to a technical space, and folders from the wrong space are findings. include/, src/, tests/ belong to C++; modeling/ belongs to codegen org models; venv/ exists only in Python tool components and never in the repository. SQL schema and populate content belongs to the SQL technical space, centralised under ores.sql (see SQL Technical Space): a C++ component that grows a create/ or populate/ tree has drifted, as does any component with a Python venv/.

2.6.5. Temporary and leftover artefacts

Scaffolding leaves placeholder artefacts behind (the scaffold's stub header and implementation, *.puml stubs, test stubs). None of them survive in a finished component. A pass looks for them and for the never-tracked names above, and removes them. A container folder that existed only for a removed artefact goes too.

find projects/ores.<name> -name '*stub*' -o -name '*.orig' \
  -o -name '*.rej' -o -name '.DS_Store' -o -name '__pycache__'

2.6.6. Standard tests

Every part exposes its tests through a CMake test target, and the tests run green under ctest. The file and case conventions are in Unit test conventions. A component with no tests at all is a finding: either it is a pure-header library that says so in its overview, or it needs tests. When codegen entities exist, per-entity integration tests are part of the standard output; a component that loses them in the generated tree is a codegen finding, not a component finding.

2.6.7. Comments

Hand-written code follows the compass-code-review-comments skill (compass-code-review-comments): comments are rare, they explain why, and they never narrate. Generated code never gets hand-edited comments. Comments in generated files are a template concern: the template is the only place to fix them, and a comment survives only when it documents the domain. Review the templates that generate the component's files the same way, as step 6 of Codegen Fix Drift does for the code itself.

2.6.8. File naming

File names are snake_case everywhere. A file name matches the type it holds, and the implementation file mirrors the header. Tests end in _tests.cpp, headers are .hpp, and an aggregate header ores.<comp>.hpp exists where the component's entry points list says so. The naming rules of Code Review Checklist are the reference.

2.6.9. Type naming

Type names are snake_case in all C++ code. Namespaces follow ores::<group>[.<part>]::<facet>. Types generated for an entity match the entity slug in its org model; a mismatch means the model, the type, or the regeneration drifted. Look for types whose name contradicts their home folder or their namespace: a misnamed type is a finding, and renaming it is trivial in a component at zero drift (regenerate, do not hand-edit).

2.6.10. Misplaced files and misnamed classes

Walk the tree with the shape in mind. A file lives where its concern lives: domain types in api, repositories in core, handlers in service. A class whose name, namespace, or folder disagree is a finding.

2.6.11. Component registries

The system indexes components in three registries. Each one must show the component and its parts:

  • The System Model layer pages (System Model) list every part of a composite, with its dependency column. A part that exists in the tree but not on its layer page is a finding: the refdata audit found ores.refdata.client missing while its siblings were listed.
  • component_catalogue.org (component_catalogue.org) maps each codegen component name to its modeling directory. Entities live at the group level of a composite; parts need no row here.
  • The component's messaging reference (linked from the part overviews' See also) lists the NATS subjects and handlers. A handler without a subject row, or a subject row without a handler, is a finding.

A stale registry entry is a finding; fix it in the same pass.

2.6.12. Model and profile hygiene

An entity model bound to a profile states only what it decides for itself. The profile carries the rest, because the generator merges a profile's * Assignments as defaults. So an explicit property line whose value equals the profile's is dead weight. It repeats the default and it invites the two to drift apart. It is a finding; remove it.

The comparison is mechanical. Read the model's :profile:, load that profile's assignments through the generator's own loader (codegen.org_loader._load_profile_assignments), and intersect the assigned feature names with the keys the model states explicitly. Reusing the generator's loader keeps the check in step with the merge the generator performs.

Three cases follow from the comparison.

  • Equal value. Remove the line. The profile becomes the single source of truth for that feature.
  • Different value. Keep the line. It is a deliberate override, and the record says why.
  • Structural property. Keep it. Profiles hold variability features only, so a property such as parent_id_column or hierarchy_name_field is never in an assignment table.

One caution applies. A profile writes each feature into its namespace, and not every consumer reads it there, so compare against the namespace the reader uses rather than the one the catalogue declares.

has_parent_id was the recorded case, and it is fixed. The merge filed the feature under sql while the templates and core.py read it at the entity root, so the explicit line was load-bearing: removing it dropped the entity's hierarchy function, repository method, handler and service. Fix the has_parent_id feature namespace so profile bindings take effect moved the feature to the root namespace, where has_tenant_id and has_workspace_id already lived, and the lines came out.

Treat a removal as output-neutral only after regeneration proves it.

The proof is regeneration, as Codegen Fix Drift describes. Commit the model edits first, because the drift check diffs the whole tree. Then run check_component_drift.py --component <slug> for the component. A component outside the known-drift-free registry cannot be proven this way, so leave its models alone until it joins.

The first pass ran on 2026-09-17. It removed 41 redundant lines across 19 models in analytics, compute, iam, refdata, reporting and synthetic. Two lines stayed as overrides: compute.result sets has_change_reason_cache false against the profile's true, and reporting.report_definition sets has_workspace_id true against the profile's false. Four has_parent_id lines stayed for the caution above; the follow-up story removed them once the namespace was fixed. The same defect had also masked eleven flat entities, which bound the hierarchy profile without the two column properties that profile requires. The story rebound those to uuid-identified-lookup.

2.6.13. Platform-specific code placement

Platform concerns have one home: ores.platform (ores.platform). It owns the cross-platform OS abstractions: environment variables, the filesystem, net, process and time facets, and the signal set a service treats as a shutdown request. A component must not inline the platform itself. Look for preprocessor conditionals on _WIN32, __linux__, __APPLE__, compiler or architecture macros (__GNUC__, _MSC_VER, __x86_64__), and for direct OS headers (unistd.h, windows.h, sys/*.h) outside ores.platform:

grep -rnE '#\s*(ifdef|ifndef|if)\s+(__|_WIN32)|<unistd\.h>|<windows\.h>|<sys/' \
  projects/ores.<name>/include projects/ores.<name>/src

An inline conditional is a finding. When the concern has no home in ores.platform yet, extend that component (add the abstraction, then the callers use it); when it has a home, route the caller through it. Both directions are mechanical; porting an existing abstraction's callers is a record-only finding when it spans many files.

2.6.14. Component documentation

Every simple component and every composite part carries a modeling/component_overview.org with the six required sections, adequately filled in for the component's content. Use the good-versus- skeleton table of Component Documentation Guide as the bar. The See also section links with org-roam id-links, never file paths; it links sibling components, and it links the knowledge documents that give the component's content its depth. A component whose overview cannot link anywhere is a sign the knowledge does not exist yet. A composite's root overview is the index shape (Summary, Sub-components, Entity modules) and is optional when the parts are documented.

Every summary of the component must describe what the parts actually do: the group overview's Summary and description, the System Model layer page's narrative under the component heading, and any brief in a registry table. A summary that contradicts a part overview is a finding; the compute audit found the layer page crediting the core with the domain model while the core overview describes cluster management. Fix the stale prose in the same pass.

2.6.15. Diagrams

Every component overview has its .puml source and a rendered image committed next to it, and the overview's Diagram section points at the image. The diagram is up to date: it shows the current parts, facets, and classes, with no stale nodes. A component draws supporting diagrams when its content warrants them, such as ER diagrams for a schema-owning component or sequence diagrams for its flows.

Authoring a diagram runs two passes. The automated pass captures most entities and some relationships: build/scripts/generate_component_puml.py parses the C++ headers and emits the type skeleton (namespaces, classes and structs with their members, base relationships) into the section above the sentinel line. The manual pass cleans up that output and makes the diagram more complete: layout, notes, the relationships the automated pass cannot infer, and any supporting shapes. The sentinel line is the contract between the two passes:

' --- manual: everything below this line is hand-authored; the script preserves it ---

A refresh rewrites only the section above the sentinel and preserves everything below it verbatim, so hand-authored content that must survive regeneration lives below it. The script resolves simple-component layouts today; parts of a composite keep the same file shape so automated support can slot in.

Follow the PlantUML class diagram conventions, the PlantUML ER diagram conventions, and render with the How do I generate PlantUML diagrams? recipe. A missing or stale diagram is a finding. The trivial fix refreshes it through the two passes in order: re-run the automated capture, then redo the manual cleanup and verify the sentinel section survived.

2.6.16. Complexity measure

The audit measures complexity; it does not fix it. The measure ranks components for the audit backlog and for later refactoring passes. lizard (https://github.com/terryyin/lizard) runs from the compass venv (see ores.compass requirements.txt), with the same command every time so numbers stay comparable across components and runs. Point it at the C++ sources: the part dirs of a composite, the include/ and src/ of a simple component:

projects/ores.compass/venv/bin/lizard \
  projects/ores.<name>/*/include projects/ores.<name>/*/src
projects/ores.compass/venv/bin/lizard \
  projects/ores.<name>/include projects/ores.<name>/src

Record per component: total NLOC, total function count, average cyclomatic complexity, and the top file by cyclomatic complexity. Keep the output in the audit record. Generated sources dominate raw totals; read them as a template signal, not a component signal, and say so in the record. No threshold gates anything in this audit. When two components are candidates for the next audit, audit the more complex one first. When a refactoring pass starts later, its backlog starts from these records.

2.7. Triage rules

Every check ends in one of three states:

  • Pass. Nothing to do.
  • Trivial, fix here. Folder or file or type renames, leftover artefacts, missing documentation and diagrams, stale registry entries, stale ground-truth documents. Fix them in this pass.
  • Finding, record only. Restructures, codegen or tooling changes, comment overhauls at template level, and every hotspot the complexity measure surfaces. Record each as a task or story with a one-line justification and the measure that motivated it. A later pass does the refactoring; this pass does not start it.

3. See also

Emacs 29.3 (Org mode 9.6.15)