Task: Check that a model's pasted block names symbols that exist
Table of Contents
This page documents a task in the Harden the gates the component clean standard rests on story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Fail when a model's pasted code block names a symbol the tree does not declare.
A model can paste hand-written C++ into generated output through an
:implements block, and the block reaches the compiler only when its component
is regenerated. So a rename in one component can leave another component's block
naming a symbol that no longer exists, and nothing notices: the generated file
still compiles because the block is not in it yet, and the build breaks only when
somebody later regenerates that component and the stale block arrives.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Harden the gates the component clean standard rests on |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-27 |
3. Acceptance
- Every qualified name a pasted block references resolves against the namespaces and types the headers declare, or the check fails and names the block.
- The identifier existing under a different namespace fails, which is the shape of the measured defect.
- It fails when it finds no block and when it finds no qualified name, so it cannot pass a tree by finding nothing.
- It runs on every change under
projects/, because a rename in any component's headers is what it is sensitive to. - A test fails if the resolution stops working, and the measured shape is one of them.
4. Plan
Why a second gate rather than the sweep. The sweep added by the catalogue sweep task catches the measured case, because the model and the generated file disagree there: refdata's generated handler was hand-edited to the new names while the model kept the old ones. It cannot catch the narrower case where a model and its generated file agree and are both stale, because the render reproduces the committed bytes. This check reads the model, so it catches both, and it needs no regeneration, which is what lets it fail at review.
How a name resolves. The check reads every header under a projects/ include/
directory and builds the set of (scope, identifier) pairs it mentions, where the
scope is the namespace, class, enum or struct path the identifier sits inside. A
pasted ores::a::b::name resolves when (ores, a, b) declares name. Two
details were found by measuring rather than by design: an export macro sits
between the keyword and the name, as in class ORES_PLATFORM_EXPORT datetime, so
the macro has to be skipped; and a type's own name has to be recorded before its
braces are entered, or ores::platform::time::datetime does not resolve.
What it does not do. It does not expand macros, follow using aliases across
translation units, or tell a declaration from a use. The scope an identifier sits
inside is recorded whether or not the identifier is declared there, and that errs
towards resolving, which keeps the check quiet on the tree it runs against. It is
a lint for a rename, not a compiler, and the honest statement of its power is
that it resolves 34 of the 34 names on this tree and fails on a name whose scope
does not declare it.
What that means for the measured defect is narrower than it sounds, and the
reason is worth recording. On the current tree
ores::service::messaging::step_id_header still resolves, because the identifier
survives somewhere under that scope, while
ores::service::messaging::instance_id_header and
ores::service::messaging::tenant_id_header do not. So the check catches a
rename when the identifier leaves its scope entirely, which is the common shape,
and not when a same-named symbol remains behind. The independent verifier could
not reconstruct the pre-migration set of names from the clean-service record, so
how many of that rename this check would have caught is not a measured number and
is not claimed here.
Why it is in the sweep workflow. A rename lands in somebody else's headers, so
the check is sensitive to any change under projects/. codegen-drift.yml fires
on the inputs codegen reads, which does not include a component's headers;
codegen-sweep.yml fires on any change under projects/, which does. It reads
models and headers only, so it needs neither codegen nor a formatter and takes
about eight seconds.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. Measured at the branch point, 87437c8748.
| What | Value | Command | Commit |
|---|---|---|---|
| Pasted blocks | 98 blocks in 21 models | check_paste_block_symbols.py |
87437c8748 |
| Qualified names referenced | 34, all resolving | check_paste_block_symbols.py |
87437c8748 |
| Declared (scope, identifier) pairs | 191,661 in 4,545 headers | check_paste_block_symbols.py |
87437c8748 |
| Check runtime | 7.5 seconds | time check_paste_block_symbols.py |
87437c8748 |
| End-to-end proof | renaming make_request_context fails with 4 blocks named, in marketdata, refdata and trading |
rename the declaration in ores.service/service/request_context.hpp and run the check |
87437c8748 |
6. Notes
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2276 | [codegen] Check that a pasted block names symbols that exist |
9. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
10. Result
projects/ores.codegen/scripts/check_paste_block_symbols.py resolves every
qualified name rooted at ores that a model's :implements block references
against the (scope, identifier) pairs the headers declare, and fails when the
scope does not declare the identifier. It reads the models and the headers only,
needs no build and no formatter, and takes about eight seconds.
It is the model-side companion to the catalogue sweep. The sweep catches the measured case because the model and the generated file disagree there; it cannot catch the narrower one where both agree and are both stale, because the render reproduces the committed bytes. This reads the model, so it catches both, and a rename fails at review with no regeneration.
Measured at the branch point: 34 qualified names referenced by 98 blocks in 21
models, all resolving, against 191,661 declared pairs in 4,545 headers. Proved
end to end by renaming make_request_context in its header, which makes the
check fail and name all four blocks that reference it, in ores.marketdata,
ores.refdata twice and ores.trading. Ten tests.
What it does not catch is recorded rather than implied:
ores::service::messaging::step_id_header still resolves on this tree because
the identifier survives under that scope, while instance_id_header and
tenant_id_header under the same scope do not. So it catches a rename when the
identifier leaves its scope entirely, which is the common shape, and not when a
same-named symbol remains behind. It is a lint for a rename, not a compiler: it
does not expand macros, follow using aliases across translation units, or tell
a declaration from a use.
It runs in codegen-sweep.yml, whose path filter is projects/**, because a
rename lands in somebody else's headers and codegen-drift.yml's filter matches
none of them. Named in V05 of the Component Clean Standard and in the
compass-pr-raise gate roster. Codegen suite 916 passed, compass lint clean,
validate_docs.sh 40 of 40.