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.

Emacs 29.3 (Org mode 9.6.15)