Component Architecture Audit
Table of Contents
- 1. Summary
- 2. Detail
- 2.1. When to run it
- 2.2. Readiness
- 2.3. How to run one pass
- 2.4. Automated checks
- 2.5. The signed-off folder catalogue
- 2.6. Manual checklist
- 2.6.1. Component shape
- 2.6.2. Sub-component structure
- 2.6.3. Folder morphology
- 2.6.4. Technical space of folders
- 2.6.5. Temporary and leftover artefacts
- 2.6.6. Standard tests
- 2.6.7. Comments
- 2.6.8. File naming
- 2.6.9. Type naming
- 2.6.10. Misplaced files and misnamed classes
- 2.6.11. Component registries
- 2.6.12. Model and profile hygiene
- 2.6.13. Platform-specific code placement
- 2.6.14. Component documentation
- 2.6.15. Diagrams
- 2.6.16. Complexity measure
- 2.7. Triage rules
- 3. See also
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
- Run the automated checks (next section). Fix nothing yet; collect the findings.
- Walk the manual checklist in order. Classify every finding.
- 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.
- 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. - 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.orgpresence, frontmatter, and the six required sections on every component overview.- A
.pumldiagram beside every component overview. - Top-level name collisions: no
ores.<a>.<b>directory next to anores.<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.clientmissing 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_columnorhierarchy_name_fieldis 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
- Component Clean Standard — the full clean-up checklist; this audit is its item H01.
- Component architecture — ground truth for component shape, the sub-component catalogue, facet placement, and scaffold profiles.
- Code Review Checklist — the naming rules and the component-only checks this audit leans on.
- Component Documentation Guide — what fills a
component_overview.orgwell. - Codegen Fix Drift — the per-component drift method this audit assumes has run.
- System Model — the cross-component index by architectural layer.
- Applied MASD — the facet catalogues that sign off facet folder names.
- SQL Technical Space — the central home of all SQL artefacts.
- ores.platform — the central home of all platform-specific code.
- Entity classification and drift baseline across all components — the program this audit runs behind.