Task: Bring ores.compute to the clean standard
Table of Contents
- 1. Goal
- 2. Status
- 3. Acceptance
- 4. Plan
- 5. Notes
- 5.1. G02, the two shapes the codegen looked like it lacked
- 5.2. A login could never find a bootstrap account
- 5.3. Three shell defects the V04 run found
- 5.4. The live run found a wire-format defect
- 5.5. The telemetry protocol is an operation model
- 5.6. The work surface wants an operation model
- 5.7. M06, the platform model
- 5.8. One codegen-wide finding from the protocol audit, and one fix
- 5.9. A junction announced an event that nothing served
- 5.10. The presentation drawer, M09
- 5.11. Regeneration, G01 and G05
- 5.12. The shell consumer migration recipe
- 5.13. B03 classification of the 52 hand-written C++ files
- 5.14. M02, the workflow batch link has no profile
- 5.15. The shell component tree, and what stays hand-written
- 5.16. The review round
- 6. Test Scenarios
- 7. PRs
- 8. Review
- 9. Result
This page documents a task in the Clean ores.compute to the component clean standard story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
ores.compute passes the Component Clean Standard for a component of kind Entity.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Clean ores.compute to the component clean standard |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-26 |
3. Acceptance
- Every checklist item that applies to a component of kind Entity passes, with its evidence recorded on the task.
- Every item that does not apply is recorded as not applicable, with the reason.
- Every generator defect found is fixed at its source, with a regression test.
- ores.compute is listed in
COMPONENTS_UNDER_TEST, andcheck_component_drift.py --allpasses.
4. Plan
Work the Component Clean Standard in its section order, one entity at a time, and record each item's result in the table below.
The standard grew five items while this task ran: the survey of four model-free infrastructure components on 2026-09-25 added B06, G07, H04, V07 and V08, so the standard now holds 47. All five apply to any component, so this task runs them too and records the result like the rest.
- Baseline and inventory (B01 to B06). Record the baseline before any edit.
- Models (M01 to M10).
- Protocol (P01 to P06).
- Generation (G01 to G07).
- Wiring and data (W01 to W04).
- Shell (S01, S02).
- Structure and hygiene (H01 to H04).
- Verification (V01 to V08), ending with the registry entry.
| Item | Result | Evidence |
|---|---|---|
| B01 | Pass | evidence/b01_component_drift_baseline.txt records 88 changed and 34 created files. evidence/b01_model_drift_baseline.txt records two known exceptions, one of them compute's result.has_change_reason_cache and the other reporting.report_definition.has_workspace_id. |
| B02 | Pass | evidence/b02_b03_b04_inventory.txt: nine models. Six entity, one junction, one module and one component overview. The module and the overview carry no variability. |
| B03 | Pass | Re-surveyed after the workflow batch link: 322 C++ files outside tests, 270 generated and 52 hand-written, against 258, 189 and 69 at the baseline. Messaging headers 45, with 5 hand-written, all five live handlers and the registrar composition point. Each of the 52 is classified below as infrastructure or dead, and none is generatable. Evidence in evidence/b02_b03_b04_inventory_after_workflow_link.txt. |
| B04 | Pass | Same file: 80 subjects across ten protocol headers, and no raw subject literal is left in hand-written code. The four that built a compute.v1.work.assignments. subject at runtime now build it from the generated constant. |
| B05 | Pass | Nothing blocks the work. PR #2138 merged, so the branch rebased onto origin/main at 5de59e3fb8 and picked up its M09 chrome removal. No other in-flight branch touches projects/ores.compute. |
| B06 | Pass | evidence/b06_consumer_census.txt: every hand-written public header that declares a symbol has a production consumer, and none is reached only from a test. The two headers with no consumer declare no symbol. |
| G01 | Pass | Regeneration is byte-clean and idempotent, and the component builds. check_component_drift.py --component compute-cpp reports no drift, and it reported no drift on the two runs before it as well. evidence/g01_regeneration_run8.txt. |
| G02 | Pass | Every file the generation supersedes is generated now, at the path it was hand-written at: the three protocol headers, the platform stack, the telemetry and work protocols, the two telemetry tables with their entities, mappers and repositories, and the workflow batch link. The 52 files left hand-written are the component's infrastructure, and the classification below says of each why generation does not supersede it. |
| G03 | Pass | 225 files under projects/ores.compute carry the generated marker and 60 do not, and the 60 are the hand-written set B03 classified. The argument that every generated file is marked is the empty drift list: the generator writes the marker, so a generated file missing it would always differ from the tree and appear there. evidence/b02_b03_b04_inventory_after_work.txt. |
| G04 | Pass | The generated output was read against the hand-written code it replaced, facet by facet, over the platform model and the two operation models. Two defects came out of it and are fixed in the templates: a response member that collided with an entity named result, and TypeScript domain imports that collided with the public types. Details in Notes. |
| G05 | Pass | Nine generator defects, fixed at source with seventeen new cases: five in org_loader.py (the version-facet suffix, the response payload member, the stated verb on a facet message, the TypeScript domain alias, and the declared key field), the junction announcement, the put arity that counted a minted id, the replace that minted one, and the table renderer's row variable. Two generator gaps were features rather than defects and carry cases too: the hypertable retention policy and the newest read. Details in Notes. |
| G06 | Pass | regenerate_cmake_component_files.py --component ores.compute.<part> --check reports every source list up to date, for all five parts and for the shell adapter part. |
| G07 | Pass | evidence/g07_generator_defects.txt: two generator defects are open, and no artefact behind either was refreshed. The puml generator resolves only projects/<name>/include/<name>/, so the shell compute diagram reports no include directory and stays hand-maintained; entity_meta_model_cpp_qt.org still emits the Qt chrome M09 removed. Every other artefact was refreshed only after its defect was fixed, which G05 records. |
| M02 | Pass | Eight of the nine models bind to a profile: six to uuid-identified-lookup, the junction to tenant-scoped-junction and the two telemetry tables to readonly-timeseries. The workflow batch link is a recorded no-match: none of the thirteen profiles describes a tenant-scoped current-state entity with no audit tail, and the closest, tenant-scoped-junction, is defined for a junction's two-sided link. Its features are stated in the model and its reason is in Notes. |
| M04 | Pass | No property sits in a namespace the loader does not read for it. The one that did was has_change_reason_cache, moved to the model root for M09, and it is now removed as dead. |
| M05 | Pass | Each model describes one table. The junction is modelled as a junction and no joined shape is modelled as an entity. No compute table is current-state, so no model needs the current-state shape. |
| M06 | Pass | Every wire type the component exchanges is modelled. The seven entities, the junction and the two operations are all org models, and the two telemetry tables are entities now, so no hand-written wire type and no hand-written protocol header remains. check_protocol_twin_coverage.py passes over both components under test. |
| M07 | Pass | The junction names its singular (#+name_singular: app_version_platform), gives both key columns their real type (uuid), and its row-level security is tenant scoped, ores_compute_app_version_platforms_tbl with a tenant_id policy and a current-tenant check. read_only, client_read_only and party_id_is_target do not apply: the junction is written by publish, and it has no party column. |
| M08 | Pass | Every cpp_type in the component is a scalar or a registered built-in, so no custom type needs a header binding. validate_schemas.sh passes with 303 tables and no warnings, so the SQL spellings are canonical. No flag is an integer. |
| M10 | Pass | The component overview declares the parts and the composite generates its root CMakeLists.txt (the file carries the generated marker). validate_docs.py passes all 41 components. |
| M01 | Pass | No JSON model remains under projects/ores.compute, and no model declares lookup_entity. Every entity is on the current org format. |
| M03 | Pass | check_model_drift.py reports no model drift and no compute exception. The one accepted exception left, reporting.report_definition.has_workspace_id, is another component's. |
| M09 | Pass | No Qt or Wt presentation drawer or property remains in a compute model. The drawer's Qt list-window columns and its retired-type class names are gone. grep -l "^\*\* Presentation" projects/ores.compute/modeling/*.org returns nothing, against 109 files elsewhere. |
| V01 | Pass | compass build over the whole tree exits 0, with the component's tests and every consumer. |
| V02 | Pass | The codegen suite passes 536 cases. All four compute suites pass against the live fleet: api 165 assertions in 54 cases, core 60 in 10, service 26 in 10, wrapper 20 in 5. One defect came out of it, the platform table's select-only RLS policy, and is fixed. Evidence in evidence/v02_test_suites.txt. |
| V03 | Pass | The database recreates from scratch, the fleet starts and serves, and a session works. compass db recreate -y -k exits 0 and the recreated platform table carries the generated notify trigger. The bootstrap commands were persisting nothing, which is fixed: an account and a tenant now exist and a login succeeds. Evidence in evidence/v03_v04_fleet.txt and evidence/v04_shell_commands.txt. |
| V04 | Pass | All 56 generated compute shell commands ran against the live fleet and answered, with no failure: the eight menus' reads, the seeded reads, and a full add, get, versions, set and delete cycle for each of the seven entities and the junction. Three defects came out of the run and are fixed at source. Evidence in evidence/v04_shell_commands.txt. |
| V05 | Pass | Every gate is green with the component under test: component drift, model drift, protocol twin coverage, handler permissions, populate references, recipe inventory and the CMake source lists, with the exceptions each gate already carries elsewhere. Evidence in evidence/v05_gates_with_compute_listed.txt and evidence/v06_registry_all.txt. |
| V06 | Pass | COMPONENTS_UNDER_TEST in projects/ores.codegen/scripts/component_registry.py lists compute-cpp, and check_component_drift.py --all passes over the list. Evidence in evidence/v06_registry_all.txt. |
| V07 | Pass | evidence/v07_vacuous_tests.txt: every test asserts a literal expected value. Each non-empty, truthiness or non-throw assertion sits beside such an assertion in the same case, so a subject that did nothing fails it. |
| V08 | Pass | evidence/v08_source_files_without_a_test.txt: all twenty hand-written src/ files, each with the test that covers it or the reason it has none. One gap is recorded: batch_workflow_bridge.cpp has no test, and the review found a tenant-scoped removal defect there. |
| P05 | Pass | Every consumer follows the protocol: the shell application and, before main deleted the component, the CLI. Nothing is carried for backwards compatibility. |
| P01 | Pass | Every audited entity speaks list, get, put and delete, and carries the versions facet for the history and point-in-time reads: compute.v1.<plural>_versions.list and .get. The junction carries neither, and no junction in the codebase does, iam's included. That is a codegen-level decision, recorded as a finding rather than fixed here. |
| P02 | Pass | No raw subject literal is left in hand-written code. The last four built a compute.v1.work.assignments. subject at runtime, and work_assignment_event::nats_subject now carries it. Evidence in evidence/b02_b03_b04_inventory_after_work.txt. |
| P03 | Pass | 80 declared subjects, 80 distinct, no duplicate. Every generated registrar subscribes to as many subjects as it has handlers (9, 8, 9, 9, 9, 10, 10). No declared record is unreachable: inventory.py walks from each subject and from every reference outside a record's own header, and reports zero. The one it found, a junction announcement, is fixed in the generator. Evidence in evidence/p03_unserved_records_after_junction_fix.txt. |
| P04 | Pass | The four non-entity-verb operations generate from operation models. ores.compute.telemetry carries the grid summary pair and the node sample publish, and ores.compute.work carries pull, heartbeat, reap and submit. No hand-written protocol header remains. |
| P06 | N/A | The component keeps no warmed event cache. No generated cache file exists in it, and none exists in any component, so the item has nothing to check. |
| W01 | Pass | The messaging registrars are composed at the core composition point and the six entities' event mappings at the service's. The junction materialises no event family, because the generator emits none for junctions, and the record that promised one is now removed from the protocol. |
| W02 | Pass | Every permission a compute handler checks is seeded, and the three roles that need them carry compute::*. Eleven codes were missing, so the seed script gains them. check_handler_permissions.py now covers two components and 160 codes. Evidence in evidence/v05_gates_with_compute_listed.txt. |
| W03 | Pass | check_populate_references.py reports that populate references resolve with compute under test. Same evidence file. |
| W04 | Pass | validate_schemas.sh reports 303 tables and no warning. The generated SQL enables row-level security for the two telemetry tables where the hand-written create file did not, so their two stale RLS_001 exceptions are gone and they take the recorded hypertable key exception beside every other hypertable. Evidence in evidence/w04_validate_schemas.txt. |
| S01 | Pass | The eight generated command units are registered in the repl and linked into ores.shell.application: the seven that were there plus platform_commands, which the platform model generated and nothing registered. The commands they supersede are deleted, and the commands no unit covers stay hand-written. |
| S02 | Pass | The recipe inventory lists 191 recipes in 110 categories and the gate passes. |
| H01 | Pass | The Component Architecture Audit ran against projects/ores.compute. Shape and part catalogue pass, every directory at depth 3 is signed off, no never-tracked file and no empty directory exists, the facade dirs mirror, and validate_docs.sh passes all 41 components. Its two real findings are fixed: the stale service and wrapper part diagrams are refreshed and re-rendered. The missing root .puml is not a finding, because a composite root's modeling/ is the group index the validator exempts. Details in Notes. |
| H02 | Pass | No Qt or Wt code, no commented-out code and no dead code remain. The one hand-written duplicate of a generated artefact is deleted: wrapper/src/app/log_publisher.hpp, a redirect header that nothing included. Details in Notes. |
| H03 | Pass | A comment-rule review of the whole component found 65 violations, and all 65 are fixed: 27 restatements and edit-history narrations in sources, tests and models, 21 end-of-line comments, and the retired-technology names in model prose, which regeneration carries into the generated headers and SQL. The one template-level violation, the AUTOMOC comment in the generated component_files.cmake, is fixed in its org source too, so compute carries none. The remaining stale copies live in 22 other components and are recorded as a codegen-wide finding. Details in Notes. |
| H04 | Pass | evidence/h04_platform_shims.txt: the only platform branches are four #ifdef _WIN32 calls into libarchive's wide-character API, which the standard library does not replace. No hand-rolled unreachable(), no no_unique_address macro, no compiler-detection header. |
Every applicable item is recorded above: 46 pass and one (P06) does not apply. The five items the standard gained on 2026-09-25 are among them.
The H01 and H03 work is a review, so the method and the findings are worth more than the verdict.
4.1. H01, the architecture audit
The automated half is validate_docs.sh, which passes all 41 components.
The manual half was walked item by item and produced two findings, both in
the diagrams beside the part overviews. ores.compute.service.puml was
written in June and had never been refreshed, so it carried neither the
seven generated *_event_registrar units in the new messaging facet nor
workunit_dispatcher; ores.compute.wrapper.puml was missing
log_publisher. Both are refreshed above the sentinel line, the
hand-authored region below it is byte-identical, and both render to their
.png again.
The capture script, build/scripts/generate_component_puml.py, resolves
simple components only, so a part of a composite is refreshed by hand.
That is a codegen-wide finding, and it is recorded here.
Two items that look like findings are not.
- The composite root has no
.puml. The audit asks for a diagram beside every component overview, andvalidate_docs.pyinspects partmodeling/dirs only; a composite root'smodeling/holds the group index and the models, and no other composite root in the repository carries one either. - The
clientpart has notests/directory, so it exposes no ctest target. Its own overview already records that as a deliberate gap, andores.iam/client,ores.workspace/{api,core,service}andores.http/coreshare it.
One audit finding was wrong and is not acted on: the reviewer expected
wrapper/include/ores.compute.wrapper/app/application_exception.hpp. No
such header exists in the wrapper, and none is missing.
4.2. H02, the legacy scan
The scan covered Qt and Wt code fragments, commented-out code and
hand-written duplicates of generated artefacts. It found one of the three:
wrapper/src/app/log_publisher.hpp, a two-line redirect to the real header
in include/, included by nobody and not listed in the generated source
list. It is deleted.
The Qt and Wt hits that remain are prose. Two of them were edit history and are rewritten with the rest of the comment pass. The rest name the retired technology only to say the component does not use it, which the comment rules allow.
4.3. H03, the comment pass
The review covered every hand-written and generated file under
projects/ores.compute/ plus its org models, against the project comment
rules. It found 65 violations in four classes: 10 comments that restate
the code, 20 that narrate edit history, 21 end-of-line comments, and 14
that name a technology the component no longer uses.
The fixes split by where the text lives. Comments in hand-written sources and tests are edited in place. Comments in generated files are not touched at all: the prose lives in the org model, so the model is rewritten and the tree is regenerated, which is what carries the fix into the C++ header and the SQL schema comment together.
One violation lived in a template rather than in the component. The
generated component_files.cmake files carried "Headers must be listed
for AUTOMOC to find Q_OBJECT declarations.", and the text comes from
library/templates/ores.cmake.component.files_src.org. The line is
corrected in the org source, re-tangled, and the source lists of compute,
iam and shell are regenerated, so compute carries no retired-technology
comment at all.
The rest of that regeneration is deferred, and this is a codegen-wide
finding. The same one-line template change reaches 145 source
lists across 22 components, and 123 of them are stale for unrelated reasons
and would pull their own drift into this branch. So the components the
branch owns are regenerated and the rest are recorded.
regenerate_cmake_component_files.py --all --check now reports 123 stale
lists outside the branch against 0 for compute, iam and shell.
5. Notes
5.1. G02, the two shapes the codegen looked like it lacked
This item was carried for two rounds as a codegen gap. It was not one, and the way it resolved is worth keeping.
5.1.1. The telemetry tables
grid_sample and node_sample were the item's centre. Six hand-written
files described them: two domain structs, two sqlgen entities and one
repository with five methods. All six are generated now.
The earlier reading was that the tables needed a fourth entity shape, an
append-only fact table with no surrogate key and no validity window.
ores.iam.session showed why that was wrong: it is a current-state entity
and a hypertable, and the codegen already expresses that. A current-state
entity takes the model's own primary key, so declaring a surrogate id
and the partition column sampled_at as key columns renders
primary key (id, sampled_at), which is what TimescaleDB requires. Four
gaps became two.
- No retention policy. The hypertable block created the hypertable and
stopped, while both hand-written tables dropped chunks after thirty
days.
:hypertable_retention_interval:renders the policy, and it reports rather than ignores the interval when the licence has no retention job. The local licence isapache, so no job is set here, which is what the hand-written block did too. - No newest read. A paged read walks the key from its oldest end, so it
cannot answer "the last sample" without reading everything before it.
:newest_by:names the column the newest is measured by and rendersread_newest, one indexed row.
Both flags are generator features with cases in
test_current_state_shape.py, and both were proved on a throwaway probe
model before either table was touched.
What stays hand-written is compute_telemetry_repository, and it is
infrastructure rather than a generatable file: a per-node newest row is a
DISTINCT ON (host_id), which is neither a read of the newest nor a read
scoped to one host, and the live summary is a call to
ores_compute_grid_stats_fn, a SQL function no template expresses. The
file went from five methods to two.
A defect came out of the work. The table renderer emitted
for ([[maybe_unused]] const auto& : v), with no variable, because the
template reads the iterator variable from the model's C++ drawer and the
loader defaulted it only for the table display items. The loader defaults
it in both places now, and the two models state theirs like every other
compute entity.
5.1.2. The workflow batch link
The last three files were the link's entity and repository. The table is a current-state entity the codegen expresses, and its model replaces all three at the same paths.
What looked like a blocker was the bridge. batch_workflow_bridge is a
service-level poller: it reads every tenant's pending links and deletes
each one when its batch finishes. Every generated read is scoped to the
caller's tenant, and the generated removal filters on the caller's tenant
as well.
Two flags answer it, and they do different jobs.
:tenant_read_scope: shared states the C++ read: no tenant filter is
added, so the row-level-security policy is what decides.
:rls_system_tenant_visible: states the policy: a system-tenant session
may read and write every tenant's rows. The service's own context is the
system tenant, so the poll sees every link. The removal must name the
link's tenant, because mutations stay tenant-scoped, so the bridge now
removes each link under the tenant that owns it, which is what the row is.
The policy was verified against the recreated database as a role that neither owns the table nor bypasses row-level security:
set app.current_tenant_id = '<the link's tenant>'; select count() from ores_compute_workflow_batch_links_tbl; – 1 set app.current_tenant_id = '<another tenant>'; select count() from ores_compute_workflow_batch_links_tbl; – 0 set app.current_tenant_id = '<the system tenant>'; select count(*) from ores_compute_workflow_batch_links_tbl; – 1
One finding came out of this and is fixed. The generator emits a notify
trigger for every entity, and the trigger is the database end of the event
pipeline: it publishes onto a channel that an event registrar serves. The
three tables whose event surface the physical space switches off had no
registrar, so each carried a trigger that published an event nothing
served. ores.sql.schema.notify_trigger is a physical-space archetype, so
the three models switch it and its drop counterpart off. No template
changed and no other component is touched.
One finding is left for its own task, and it is in the tooling rather than
the generator. The schema validator did not see the hand-written link
table at all, because its CREATE TABLE was spelled in the hand-written
style; the generated file is spelled in the parsed style, so the table is
checked now. A create file the parser cannot read is silently unchecked,
which is worth its own look.
5.2. A login could never find a bootstrap account
V03 needs a session, and a session needs an account. The commands that create one reported success and left the tables empty, so the fault read as a database problem. It was not.
A principal is "username@hostname". The hostname routes a request to a tenant; the username is what an account row is stored under and what a login looks up. The login handler split the principal on its last '@' and looked up the part before it. The bootstrap handler did not split it at all, so `create-initial-admin super_admin@localhost` wrote an account whose username was `super_admin@localhost`, and every login looked for `super_admin` and found nothing. The provisioning path had the same gap, in the account and in the party association's audit columns.
The rule was written out at five other call sites and missing at these
two, which is exactly how a rule that lives nowhere gets forgotten. It now
lives in one place, ores.iam.core/messaging/principal.hpp, as
split_principal and username_of, and all seven sites call it. The
bootstrap handler keeps the full principal only where a principal is
genuinely wanted.
5.3. Three shell defects the V04 run found
Running every generated compute command against the live fleet found three defects the codegen suite did not, because the suite read the projection and never ran the command.
- A create stated one argument more than it read. The `add` command checked for and printed `write_arity + 2` positionals, where `write_arity` counted the whole write record, but the id is minted and never read. The command therefore refused the invocation its own help printed, and accepted one only if the first value was a decoy. The count is now the fields a caller types.
- A replace could not address a row. `set` minted a fresh id, so the store's trigger saw a row no current row matched, took the write for a create, and collided with the natural key's unique index. A replace now takes the id from the caller, which is what the read it follows returns, and its help says so.
- The platform menu was never registered. The platform model generated
platform_commandsand nothing added it to the repl, so every platform command answered "Wrong command". S01 counted seven units; there are eight.
All three are fixed in the generator, with cases in
test_shell_entities.py, and the whole sweep passes.
5.4. The live run found a wire-format defect
The fleet was started for V03 and the compute service log showed the node samples arriving and failing:
[WARN] [ores.compute.messaging.telemetry_handler] Failed to decode node_sample_message: Could not parse document: unexpected character, expected a JSON value
The process wire format is msgpack (ORES_NATS_WIRE_FORMAT in .env),
and every other handler decodes through
ores::service::messaging::decode, which reads the process codec.
telemetry_handler::ingest_node_sample was the one handler that called
rfl::json::read on the raw bytes, so it could only ever read a JSON body.
Every node sample the wrappers published was dropped, and nothing failed
loudly, because a handler that cannot decode a fire-and-forget publish has
nobody to tell.
The handler now calls decode<node_sample_message>(msg) like its
siblings, so it follows the codec the publisher used. No test covered this:
the handler has no unit test, and the eventing integration tests register
their own mappings and never exercise the telemetry ingest path. Only the
running fleet showed it, which is the lesson sprint 25 recorded.
5.5. The telemetry protocol is an operation model
The blocker above stops the telemetry entities, not the telemetry
protocol. The two are separate models, and the protocol does not need
the repository read that the entities do. ores.compute.telemetry is an
operation model (#+type: ores.codegen.operation) with three messages: a
request and reply pair for the grid summary, and a fire-and-forget publish
for the node samples. The generated header keeps the hand-written file's
name, order and fields, so no consumer changed, and the check-in diff is
the generated marker, the doc comments and one added constant.
The handler stays hand-written beside the model. An operation model
generates its protocol, its TypeScript twin and, when
:ores.cpp.shell-command.enabled: is true, its shell command units. It
does not generate a service handler. ores.iam.account_messages shows
this: account_operations_protocol.hpp carries the generated marker and
account_operations_handler.hpp does not. So the read the codegen lacks
does not block the protocol, and the model lands while the entity
question stays open.
The model states :auth: none on node_sample_message. A wrapper node
publishes a sample before it has a session, and ingest_node_sample
reads it without one, so the message is public. The loader copies that
fact to requires_session, which both protocol twins emit and which the
shell command generator reads. The hand-written header stated nothing, so
both twins claimed a session the publisher never sends.
5.6. The work surface wants an operation model
pull, heartbeat, reap and submit are orchestrations rather than
entity verbs, so P04 puts them in an operation model
(#+type: ores.codegen.operation) rather than an entity. iam's
account_messages.org is the template. The model generates the protocol
and, when the shell flag is set, the shell command units. It does not
generate the service handler, so work_handler.hpp stays hand-written
beside the model and only the protocol moves. That removes the four raw
subject literals in work_protocol.hpp, so P02 follows it.
An earlier version of this note said the operation model generates a handler too. The telemetry model disproved that, and the account model confirms the correction.
5.7. M06, the platform model
platform was the component's last hand-written entity, and the smallest:
a domain class, a protocol header with one message pair, an entity row, a
repository with one read, and a handler with one verb. That is why it went
first.
Two decisions shaped the model. is_active is a plain column and not a
declared filter, because the generated read answers a filter with
filter_not_supported, and advertising a filter the service refuses is
worse than offering none. And the natural key is code, declared on the
model root, so callers address a platform by its triplet.
The coupling that could have blocked it did not. The junction surfaces the
platform code through a join, and that join is declared on the junction
with enrich_code, references and code_column, so it is generated and
names the platform table directly. A platform model does not disturb it.
The table and the seed are unchanged, so the recreation is unchanged, and the generated SQL now carries the natural-key uniqueness index the hand-written table did not.
Still hand-written: the telemetry handler, and work_protocol.hpp with
its three handlers. The work surface is the harder of the two, because
pull, heartbeat, reap and submit are operations rather than entity verbs,
so it wants an operation model under P04 rather than an entity.
5.8. One codegen-wide finding from the protocol audit, and one fix
The junction history gap shows up in iam as well, so it is not a compute defect and it is not fixed here.
- A temporal junction exposes no history read.
ores_compute_app_version_platforms_tblcarriesversion,valid_fromandvalid_to, but its protocol has no versions facet, and neither does any other junction. So a junction's rows have history that no read reaches. P01 only requires the point-in-time read where the model offers it, which is why the item passes, but the offer is missing for a table that keeps the history.
5.9. A junction announced an event that nothing served
The junction protocol declared app_version_platform_event, and the same
defect shows in iam's account_party_protocol.hpp. An entity's
announcement earns its place because a generated event registrar publishes
it on three action subjects. No event registrar is emitted for a junction,
so the record was a wire type no code path read and no subject carried.
P03 says to remove what nothing serves, so the removal is in the
derivation: junction_protocol_messages drops the announcement from the
entity shape it projects a junction onto. The fix serves dq, iam,
marketdata and refdata as well, because they have junctions too. Only
compute and iam have a drift-free C++ tree, so only those two were
regenerated; dq, marketdata and refdata already differ from their models
and carry the stale record until their own clean-up regenerates them.
The check is a rerunnable lever rather than a reading.
inventory.py walks from each subject-bearing request, follows every
record a served record carries, and adds every record named in a file
other than the one that declares it. It reports the records nothing
reaches, and it reports zero for compute. Before the fix it reported
exactly one.
The regression test asserts the junction derives no announcement on either twin, and the message-set test now expects the record's absence rather than its presence.
5.10. The presentation drawer, M09
Two properties in the drawer were load-bearing and could not be deleted
with it. key_field is the entity's declared key on the wire, read by
declared_key_field and driving the protocol key record. It now sits on
the model root, and the loader reads the root first and falls back to the
drawer, so a model can drop the drawer without losing its key.
has_change_reason_cache: false on result is a deliberate override the
model documents; it moved to the root too and the model-drift gate still
counts it as the same accepted exception.
The test that settled it. Removing the drawer alone changed eleven files,
all through the key. Removing it with key_field on the root changes
none: the regeneration is byte-identical, so the drawer was inert once its
two properties were rehomed.
One gap remains outside this component.
projects/ores.codegen/modeling/entity_meta_model_cpp_qt.org is the
meta-model a new entity is scaffolded from, and it still writes
window_title, settings_group, icon and item_var into the drawer it
emits. A model scaffolded from it today would reintroduce exactly what M09
removes. That file is codegen's own model, so it belongs to a tooling
change, not to this story.
5.11. Regeneration, G01 and G05
The component's whole generated tree drifted from its models, so the first
move was to regenerate it and build. The build failed in
ores.compute.core.lib with 13 errors. Five generator defects were
behind them, all in
projects/ores.codegen/src/codegen/org_loader.py and the templates it
feeds. Each is fixed at source, with eleven cases in
projects/ores.codegen/tests/test_protocol_name_collisions.py.
- The version facet collides with a real sibling entity. The loader
derives
app_version_keyandlist_app_versions_requestfor theappentity's version history, and theapp_versionentity derives the same names for itself. The facet now reads the component's entity set and takes the plural as its stem when a sibling named<singular>_versionexists. iam hides this because it has no entity namedaccount_version. - The response payload member collides with the envelope result member.
Every response carries
ores::utility::domain::result resultbeside a payload member named after the entity singular. For theresultentity both members were namedresult. The payload member is nowresult_value, derived once inresponse_payload_member. - The versions verb was inferred from a name suffix. A request whose
name ended in
_versions_requestwas read as the versions sub-resource, so theapp_versionentity's own list rendered the version body and could not compile: norequest.key, noresponse.versions. The verb is now stated on the message that answers a sub-resource. - The service body assumed the payload member was the entity name. It
assigned into
response.resultfor theresultentity. Both payload sites now readpayload_memberfrom the same derivation the protocol header reads. - The modified-by key could not be built.
result.orgdeclared:key_field: modified_byin its presentation drawer.modified_byis an audit column and not a column of the model, so the key record fell back to the primary key while the finders still builtkey.modified_byandread_any_by_modified_by. The declaration is removed, so result is addressed by its storage key.
check_component_drift.py --component iam-cpp reports no drift both times
it was run, so the shared codegen is self-consistent for the component
already under test and every change is confined to the shapes compute
exposes. Control output in evidence/g01_iam_drift_control.txt.
The hand-written handlers then had to follow the regenerated services,
which take a uuid where they took a string. Six call sites in
result_submit_handler.hpp, work_handler.hpp,
workunit_dispatcher.cpp and batch_workflow_bridge.cpp were migrated.
With all of that, the component regenerates and all five of its libraries build. The regeneration was reverted once, before the fixes, because a commit that does not build is not a verifiable unit.
5.12. The shell consumer migration recipe
compute_commands.cpp needs three kinds of edit.
- Rename the retired request types.
get_apps_requestbecomeslist_apps_request, and the same forapp_versions,hosts,batches,workunitsandresults.get_app_version_platforms_by_app_version_requestbecomeslist_app_version_platforms_by_app_version_id_request.save_app_request,save_app_version_request,save_batch_requestandsave_workunit_requestbecome theirput_counterparts. - Read the outcome from the envelope. The retired responses carried
successandmessage; the canonical response carriesresultof typeores::utility::domain::result. So!resp->successbecomesresp->result.outcome !ores::utility::domain::outcome::ok=, andresp->messagebecomesresp->result.message. - Reshape the writes. The retired save request wrapped the domain object
in a
datamember with afromhelper. The canonical put request carries achangerecord, which is a write plus a precondition, and anintent. The generatedapps addunit inprojects/ores.shell/compute/src/app/commands/compute/app_commands.cppbuilds exactly that, so the migration follows it.
The replace_app_version_platforms_by_app_version operation is retired
with no replacement, so publish_package composes its writes and
removals, or drops the replace step.
The migration is done. The seven generated units are registered in
repl.cpp and linked into ores.shell.application, and the commands they
supersede are deleted: list-apps, list-app-versions, list-hosts,
list-batches, list-workunits, list-results, add-batch and
delete-host. publish-package, dispatch-batch, list-platforms,
grid-stats, download-input and download-output stay, because no
generated unit covers them.
ores.cli was a second consumer of the retired replace-by-app-version
operation, in its app-version import, and it writes its junction rows
through the canonical per-row write.
publish-package also lost its read-and-preserve step. That step existed because the retired replace swapped the whole platform set for a version; a per-row write replaces one row, so the other platforms are untouched and there is nothing left to preserve.
5.13. B03 classification of the 52 hand-written C++ files
The baseline held 69, and 17 are generated now. The platform model replaced five at their own paths, the two operation models replaced three protocol headers, and the telemetry tables and the workflow batch link replaced nine more domain, entity, mapper and repository files when their entity models landed.
Nothing generatable is left. The 52 fall into five classes.
- Messaging handlers the operation models leave hand-written:
telemetry_handler.hpp,work_handler.hpp,report_submit_handler.hppandresult_submit_handler.hpp. An operation model generates its protocol and its shell commands, not a service handler. - The registrar composition point:
registrar.hppandregistrar.cppfold the generated per-entity registrars together. A registrar is generated per entity; the point that composes them is not. - Analytical reads the codegen does not express:
compute_telemetry_repository.hpp/.cpp. A per-node newest row is aDISTINCT ON (host_id)and the live summary callsores_compute_grid_stats_fn. - Service and wrapper plumbing:
application,host,main.cpp,compute_grid_poller,batch_workflow_bridge,workunit_dispatcher,package_publisher,log_publisher,archiverandhttp_client. These are processes and transports, not rows. - Scaffolding: the
export.hppfiles, the component umbrella headers,compute_storage.hppand everyconfig/options and parser pair. A build and a command line are not a model.
Nothing in the component is dead: every file that remains is reached by a caller.
5.14. M02, the workflow batch link has no profile
The model binds to none of the thirteen profiles, and it is recorded as a
no-match rather than forced onto the nearest one. The table is a
tenant-scoped current-state entity with no audit tail. The closest
profile, tenant-scoped-junction, assigns only has_tenant_id: and is
defined for a junction's two-sided link, not for an entity keyed by one
column. readonly-timeseries is for an append-only measurement table, and
the link is deleted rather than kept. uuid-identified-lookup fixes a
change-reason cache this table has no column for, which the model-drift
gate would report as an override.
The model states its own features instead, and
check_model_drift.py reports no drift because an unbound model makes no
claim the gate can check. ores.iam.login_info and ores.iam.session are
current-state entities that bind no profile either, so the no-match is the
established treatment rather than a new one.
5.15. The shell component tree, and what stays hand-written
Regeneration created projects/ores.shell/compute/ with the seven entity
command units and their recipes and nothing else: no CMakeLists.txt, no
modeling/ directory and no tests/main.cpp, and
projects/ores.shell/CMakeLists.txt did not list compute. Every other
shell component has that scaffold, so the units could not compile until it
was added. ores.compute joined the shell under compute/ with its own
group overview and diagram, and the eight units, including the platform
unit the model later generated, are registered in the repl and linked into
ores.shell.application.
A hand-written compute command family sat beside them, listing apps, app
versions, platforms, hosts, batches, workunits and results, publishing a
package and downloading bundles. The generated units supersede the list
verbs, so those commands are deleted and the hand-written file keeps the
five verbs no entity model covers: publish a package, dispatch a batch,
watch the grid, and download a job's input or a result's output. Each
orchestrates several entities rather than one, which is why no unit
carries it. The hand-written list-platforms verb went as well, once the
platform model generated a menu that streams the same table.
5.16. The review round
An independent review of the branch ran before the PR, on the standards axis and the spec axis. It found six things worth acting on, and all six are fixed.
- The bridge removed a link as the wrong tenant.
batch_workflow_bridgeread every tenant's pending links as the system tenant, then removed the finished link as the system tenant too. The generated removal is tenant-scoped, so that DELETE matched nothing while returningremoved: the link survived and the bridge republishedstep_completed_eventon every poll. The stale-link path two lines above already used the link's own tenant; the success path now does too, and it reads the returned status and warns when the removal did not happen. G02's note recorded the intent correctly all along; the code did not follow it everywhere. - The platform event mapping was never registered. The platform model generated a registrar, and the application wired the other six but not that one, so a platform change reached the in-process bus and never NATS. W01 claimed the pipeline was complete; it was not. The registrar is registered with its siblings now.
- The platform policy let a tenant delete a system row. The policy was
widened to
for allso a tenant could insert its own platform row, but a singlefor allpolicy uses oneusingclause for select, update and delete, so it also allowed deleting or re-tenanting the seeded system rows. It is now one policy per command: reads see the system rows, and every write is the tenant's own. - Five of the standard's items were unrecorded. The standard gained B06, G07, H04, V07 and V08 on 2026-09-25, after the Plan table was written. All five apply to any component, so all five were run and recorded, with evidence. The table now carries 47 items: 46 pass, one N/A.
- Two records contradicted the code. The registry comment still called G02 the one item compute does not close, and V04 counted 57 commands where the evidence has 56. Both are corrected.
- Comment and dead-code findings. The bridge and application carried
history narration,
application.cppincluded seven headers it no longer used,postgres_event_sourcekept an unreachableon_entity_change,validation_ignore.txtkept a dangling comment block, and three documents named things the tree does not have (save_app_request,ir-swap, and a claim that a missing package logs nothing). All are fixed.
The review also confirmed the parts that matter: the runbook's command
signatures match the shell, the seed's SHA256 equals the vendored
tarball's, the CSV row and column counts match the Academy example, and
COMPONENTS_UNDER_TEST carries compute-cpp with the drift gate clean.
6. 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 |
|---|---|---|
7. PRs
| PR | Title |
|---|---|
| #2157 | [compute] Clean ores.compute to the component clean standard |
8. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | The bridge removes the finished link with the service's own tenant, so the tenant-scoped DELETE matches nothing and the link is republished every poll. | projects/ores.compute/service/src/app/batch_workflow_bridge.cpp |
Accept | Fixed: the removal uses the link's tenant, and it reads the returned status and warns when the row survived. |
| 2 | The generated platform event registrar has no caller, so platform changes never reach NATS while W01 claims the pipeline is complete. | projects/ores.compute/service/src/app/application.cpp |
Accept | Fixed: registered with the other six, and its subscription is released with them. |
| 3 | The platform policy is for all, so a tenant can delete or re-tenant the seeded system rows. |
projects/ores.sql/create/compute/compute_rls_policies_create.sql |
Accept | Fixed: one policy per command. Reads see the system rows, and insert, update and delete are the tenant's own. |
| 4 | Five standard items (B06, G07, H04, V07, V08) are absent from the Plan table, Notes and evidence. | doc/agile/versions/v0/sprint_26/clean-compute/task_clean_compute.org |
Accept | Fixed: all five run and recorded, with evidence. The table now holds 47 items. |
| 5 | The registry comment calls G02 the one item compute does not close, contradicting the G02 row and its note. | projects/ores.codegen/scripts/component_registry.py |
Accept | Fixed: the comment records what stays hand-written instead. |
| 6 | V04 says 57 commands ran; the evidence has 56. | doc/agile/versions/v0/sprint_26/clean-compute/task_clean_compute.org |
Accept | Fixed: 56. |
| 7 | The platform diagram omits platform_commands, and the shell component overview narrates its own history and claims the hand-written family was replaced. |
projects/ores.shell/compute/modeling/ |
Accept | Fixed: the class and its relation are added, and the overview states what the generated part covers and what the compute menu keeps. |
| 8 | History narration and dead code: comments in application.cpp and the bridge, seven unused includes, the unreachable on_entity_change, a dangling comment block, and three documents naming symbols the tree does not have. |
several | Accept | Fixed. |
| 9 | The batch workflow bridge has no test; the tenant defect above is what the lack let through. | projects/ores.compute/service/src/app/batch_workflow_bridge.cpp |
Accept | Deferred: recorded as a capture, and listed as the one V08 gap. |
| 10 | The shell script library does not carry the generated entity recipes' scripts. | projects/ores.shell/scripts/library/ |
Accept | Pre-existing and codegen-wide (793 scripts): recorded as a capture rather than pulled into this branch. |
| 11 | The eventing listener change is outside the compute component, so it reads as scope creep against the task's goal. | projects/ores.eventing/core/src/service/postgres_event_source.cpp |
Accept, kept | The change is what makes the grid dispatch at all: the trigger publishes the canonical notification and the listener dropped it. The conversion keeps the 118 registrations that still use the older mapping working, and the migration that deletes the older API is captured. |
9. Result
ores.compute is at the Component Clean
Standard. All 47 items are recorded on this task: 46 pass and P06 does
not apply, because the component keeps no warmed event cache. Every
generator defect found on the way is fixed at its source with a case, and
the two defects still open in the tooling are recorded with the artefacts
they hold back rather than papered over.
The component is listed in COMPONENTS_UNDER_TEST as compute-cpp, and
check_component_drift.py --all reports no drift with it under test. The
build and the suites are clean.
The task also brought the grid up and ran it end to end, which the
clean-standard items alone would not have done: the ORE engine package
published to storage with the SHA256 its platform row states, the vendored
TA002_IR_Swap sample dispatched through the seeded 1.8.17.0 app version,
15 jobs drained with 15 successes, and the downloaded output matching the
Academy example's ExpectedOutput on every shared column. The procedure is
doc/llm/runbooks/run_compute_end_to_end_job/runbook.org, and the run's
output is evidence/e2e_grid_run.txt.
The review round before the PR found a real defect the gates could not: the workflow bridge removed its link as the wrong tenant, so a finished batch republished its completion event forever. It is fixed, with the platform registrar and the platform policy's write scope. Notes record the round.
What stays open is recorded as captures rather than claimed here: the
bridge has no test, the shell script library is missing the generated
entity recipes' scripts, 118 registrations still use the older event
mapping, and a current-state repository reports removed for a delete that
matched no rows.