Task: Bring ores.compute to the clean standard

Table of Contents

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, and check_component_drift.py --all passes.

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.

  1. Baseline and inventory (B01 to B06). Record the baseline before any edit.
  2. Models (M01 to M10).
  3. Protocol (P01 to P06).
  4. Generation (G01 to G07).
  5. Wiring and data (W01 to W04).
  6. Shell (S01, S02).
  7. Structure and hygiene (H01 to H04).
  8. 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, and validate_docs.py inspects part modeling/ dirs only; a composite root's modeling/ holds the group index and the models, and no other composite root in the repository carries one either.
  • The client part has no tests/ directory, so it exposes no ctest target. Its own overview already records that as a deliberate gap, and ores.iam/client, ores.workspace/{api,core,service} and ores.http/core share 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 is apache, 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 renders read_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_commands and 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_tbl carries version, valid_from and valid_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_key and list_app_versions_request for the app entity's version history, and the app_version entity 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>_version exists. iam hides this because it has no entity named account_version.
  • The response payload member collides with the envelope result member. Every response carries ores::utility::domain::result result beside a payload member named after the entity singular. For the result entity both members were named result. The payload member is now result_value, derived once in response_payload_member.
  • The versions verb was inferred from a name suffix. A request whose name ended in _versions_request was read as the versions sub-resource, so the app_version entity's own list rendered the version body and could not compile: no request.key, no response.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.result for the result entity. Both payload sites now read payload_member from the same derivation the protocol header reads.
  • The modified-by key could not be built. result.org declared :key_field: modified_by in its presentation drawer. modified_by is an audit column and not a column of the model, so the key record fell back to the primary key while the finders still built key.modified_by and read_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.

  1. Rename the retired request types. get_apps_request becomes list_apps_request, and the same for app_versions, hosts, batches, workunits and results. get_app_version_platforms_by_app_version_request becomes list_app_version_platforms_by_app_version_id_request. save_app_request, save_app_version_request, save_batch_request and save_workunit_request become their put_ counterparts.
  2. Read the outcome from the envelope. The retired responses carried success and message; the canonical response carries result of type ores::utility::domain::result. So !resp->success becomes resp->result.outcome ! ores::utility::domain::outcome::ok=, and resp->message becomes resp->result.message.
  3. Reshape the writes. The retired save request wrapped the domain object in a data member with a from helper. The canonical put request carries a change record, which is a write plus a precondition, and an intent. The generated apps add unit in projects/ores.shell/compute/src/app/commands/compute/app_commands.cpp builds 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.hpp and result_submit_handler.hpp. An operation model generates its protocol and its shell commands, not a service handler.
  • The registrar composition point: registrar.hpp and registrar.cpp fold 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 a DISTINCT ON (host_id) and the live summary calls ores_compute_grid_stats_fn.
  • Service and wrapper plumbing: application, host, main.cpp, compute_grid_poller, batch_workflow_bridge, workunit_dispatcher, package_publisher, log_publisher, archiver and http_client. These are processes and transports, not rows.
  • Scaffolding: the export.hpp files, the component umbrella headers, compute_storage.hpp and every config/ 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_bridge read 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 returning removed: the link survived and the bridge republished step_completed_event on 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 all so a tenant could insert its own platform row, but a single for all policy uses one using clause 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.cpp included seven headers it no longer used, postgres_event_source kept an unreachable on_entity_change, validation_ignore.txt kept 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.

Emacs 29.3 (Org mode 9.6.15)