Task: Bring ores.iam to the clean standard

Table of Contents

This page documents a task in the Clean ores.iam to the component clean standard story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

ores.iam passes the Component Clean Standard for a component of kind Entity.

2. Status

Field Value
State STARTED
Parent story Clean ores.iam to the component clean standard
Now The survey, the fixes and the verification are in, on the branch rebased onto origin/main. Thirteen defects are fixed at source, including the one that matters most: the component published no entity change events at all, now proved against the running fleet by 19 events on NATS. The whole tree builds; all four iam suites pass; 158 shell recipes replay with zero unwired commands. The items that do not pass are recorded individually below, each with its evidence and its reason. One defect in the environment is not this component's: ores.workflow's invalid foreign key stops the database recreating on main, for every component, so iam's V03 was verified with that one statement locally neutralised.
Waiting on A decision on the open items, and on the registry question V06 puts to the review.
Next The review accepts the open items as exceptions, or the registry entry is withdrawn until they close.
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.iam 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.

  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 G08).
  5. Wiring and data (W01 to W04).
  6. Shell (S01, S02).
  7. Structure and hygiene (H01 to H05).
  8. Verification (V01 to V08), ending with the registry question.

The standard holds 49 items. An earlier draft of this task listed 47, stopping at G06 and V06, so G07, G08, H04, H05, V07 and V08 were absent from it. All 49 are worked below.

Item Result Evidence
B01 Pass. Baseline recorded before any edit, then re-measured after each model change. check_component_drift.py --component iam --dry-run; build/audit_iam/baseline_drift_iam.txt; the repo-wide --all --dry-run is also clean: build/audit_iam/baseline_drift_all.txt
B02 Pass. 30 .org files in the modeling directories: 10 entity, 1 junction, 9 operation, 1 module, 5 component, 4 knowledge documents. Only entity and junction carry variability; operation, module and component are structurally excluded, and _apply_profile runs only on entities. build/audit_iam/survey_models.md
B03 Pass. 429 .hpp/.cpp files: 302 generated, 127 hand-written (96 production, 31 test). Classified: 31 generatable, 2 dead, 63 infrastructure, 31 test. The actions are under G02 and H02. build/audit_iam/survey_hygiene.md
B04 Pass. 151 owned subjects, each declared once in the models (36 explicit :subject:, 85 generated CRUD, 30 event). 68 messaging headers, 11 hand-written. Every raw subject literal classified; two are legitimate layering exceptions. build/audit_iam/survey_protocol.md
B05 Pass. In-flight read. feature/verify-iam-against-the-specification has nothing ahead of main. Two open captures touch this component and are deliberately untouched here: accounts_list_rejects_a_valid_session (the hand-written list handler decodes offset and limit and ignores them) and change-password-ignores-the-current-password (a real security defect in an iam operation). The clean_standard_undercounts_its_items capture is already satisfied: the standard says 49 and its table holds 49. git log origin/main..origin/feature/verify-iam-against-the-specification; doc/agile/product_backlog/inbox/
B06 Findings recorded. 233 public headers, 586 namespace-scope entry points plus 22 operator<<: production 294, internal 219, test-only 18, none 55. Test-only: 13 permission_codes and 5 role_codes constants, whose intended consumer is each owning component's authorization check – those hardcode the literal instead. None: 53 permission_codes constants (the SQL seeds the strings and no C++ reads the constant), session_operations_protocol.hpp::session_view (already recorded dead in sprint 25), application_exception (never thrown or caught; its unused include appears in twelve components' application.cpp), and six dead *_table_io pairs. build/audit_iam/survey_symbols_coverage.md; build/audit_iam/census.py
M01 Pass. No JSON model and no lookup_entity remains; every model is on the current org format. build/audit_iam/survey_models.md
M02 Fix, with two no-matches recorded. Sprint 25 surveyed and bound the six orgs that existed then; five were added afterwards and never bound. session is now bound to readonly-timeseries (verified zero-diff; the profile fixes only has_tenant_id and has_workspace_id, both already resolved) and role to uuid-identified-lookup (bisect verified: the bind's only delta is the eventing integration test the profile enables, which is a real gap and is now committed). account and permission are recorded as genuine no-matches with their feature values: account carries read_only and generate_touch_function and has no pagination; permission sets no_audit_columns and has no change-reason columns, which contradicts the profile's has_change_reason_cache. One caution from this pass: a zero-diff bind does not by itself prove a feature match. Binding permission to simple-lookup, which fixes has_uuid_primary_key to false, is also zero-diff, so the profile's assignments are inert for that entity. The decisive control that the mechanism is live: binding permission to self_referencing_hierarchy changes six files. git log of the sprint-25 bind record (six orgs); build/audit_iam/{session,role,account,permission,control}_bind_test.txt
M03 Fix. account_party_junction stated #+has_tenant_id: true, which its tenant-scoped-junction profile already fixes to true. Removed; regeneration byte-identical. The drift gate reads drawers, so the frontmatter copy was invisible to it. projects/ores.iam/modeling/ores.iam.account_party_junction.org
M04 Pass. Every stated feature sits in the drawer the loader reads per _FEATURE_NAMESPACE; no parent, RLS or party feature is stated at all, so none can be misplaced. build/audit_iam/survey_models.md
M05 Pass. Every model column maps one-to-one to its generated table. The denormalised shapes are messages, not entities: role's permission_codes and session's party_id, visible_party_ids and username. build/audit_iam/survey_models.md
M06 Pass. 20 generated *_protocol.hpp (11 entity and junction, 9 operation), every one twinned, every :response: resolves, no hand-written wire struct. The dead hand-written umbrella is deleted under G02. build/audit_iam/survey_models.md; check_protocol_twin_coverage.py
M07 Pass. The junction names its singular account_party, gives both keys their real type, and is tenant-scoped in RLS. The three write flags do not apply: generation is disabled for it and the hand-written stamp_account_party preserves the target. iam_rls_policies_create.sql; build/audit_iam/survey_models.md
M08 Fix. login_info's three flags were modelled :type: integer with :cpp_type: bool, and the DDL emitted integer. They are boolean now, per the Booleans rule in the PostgreSQL architecture page. The change followed through everywhere it had to: the partial index predicate locked = 0 became locked = false (Postgres cannot compare boolean to integer, so the index would have failed), and the two hand-written provisioners and the web seed script that inserted 0 into those columns insert false. Recorded, not fixed: boost::asio::ip::address is absent from cpp_custom_types.org's catalogue and works only through hand-stated includes. projects/ores.iam/modeling/ores.iam.login_info.org; iam_login_info_create.sql; iam_system_provisioner_create.sql; iam_publish_from_dq_create.sql; seed-test-account.sh
M09 Pass after two fixes. No Qt or Wt key, no property naming a retired message type, no retired feature, no dead history property. Two stale paragraphs were corrected at source: role.org and session.org claimed their generated CRUD handler was suppressed in favour of a hand-written one, which had not been true for some time. The claim had propagated into the generated role.hpp and both create scripts. projects/ores.iam/modeling/ores.iam.{role,session}.org; git diff of role.hpp, iam_roles_create.sql, iam_sessions_create.sql
M10 Fix. The composite declares its kind and parts, and all four sub-component overviews carry the six sections and a Diagram. Corrected: the module catalogue listed 4 of 11 entities and now lists 11; the api overview called itself header-only while src/ builds 46 translation units; and the messaging reference is dealt with under H05. projects/ores.iam/modeling/ores.iam.module.org; projects/ores.iam/api/modeling/component_overview.org
P01 Partial, recorded. The canonical protocol is generated and wired for all 11 resources: 121 request subjects, all subscribed. Two exceptions need a decision. account is :read_only:, so its writes leave the canonical protocol for the bespoke iam.v1.accounts.{save,update,delete,lock,unlock,reset-password,...} operation verbs with success=/=message envelopes. And account_contact_information declares :list_by_as_of: true, which generates list_..._by_account_id_as_of with no caller and an as_of that never reaches the wire; no iam entity sets has_as_of_lookup, while refdata's does. build/audit_iam/survey_protocol.md
P02 Fix, with exceptions recorded. All 151 owned subjects are declared once and there is no duplicate inside the component. The dead hand-written umbrella is deleted. One raw literal is fixed: the registrar subscribed to ores.variability.system_setting_changed as a string where event_traits<...>::name exists. Two served subjects are not declared in an iam model and are recorded as layering exceptions: iam.v1.history.get (derived by ores.history) and iam.v1.auth.public-key (owned by ores.nats, below the api). Grammar defects recorded for a house-wide decision: the hyphenated iam.v1.account-contact-informations.publish-from-dq, and iam.v1.login_info.*, whose resource is not plural. projects/ores.iam/core/src/messaging/registrar.cpp; build/audit_iam/survey_protocol.md
P03 Fix, with findings recorded. No duplicate subjects and no unserved request subjects inside iam. The defect that mattered: all ten generated event registrars were defined and none was called, so 30 event subjects were declared and nothing served them – see W01. Recorded: the ores.variability.system_setting_changed subscription is dead because nothing publishes that event (ores.variability declares it and never registers the mapping), which is that component's defect, so the subscription is kept and its literal fixed rather than deleted; iam.v1.auth.public-key has no modelled response and answers in ad-hoc JSON from ores.nats' jwks.cpp; three hand-written event types (role_assigned, role_revoked, account_permissions_changed) have test-only consumers. build/audit_iam/survey_protocol.md
P04 Partial, recorded. Structurally correct: all nine non-entity areas are ores.codegen.operation models and generate their headers, and no hand-written protocol header remains. Two conventions are recorded rather than changed here: the 36 operation subjects use no .v1.ops. namespace (there are zero estate-wide), and 23 operation responses carry success=/=message where all 85 entity CRUD responses carry ores::utility::domain::result. The account_party junction has a generated protocol but a hand-written handler and service, because the template cannot yet express its workflow step. build/audit_iam/survey_protocol.md; projects/ores.iam/modeling/ores.iam.account_party_junction.org
P05 Mostly pass, findings recorded. Every consumer follows the protocol in the same change and no compatibility shim or subject alias survives; the junction rename left no alias behind. Recorded against other components: ores.web keeps a hand-written duplicate wire layer (ten raw iam subject literals plus roughly 480 lines of zod bodies) beside the generated TypeScript twins in the same package; and stale consumer prose survives in provision_parties_workflow.hpp, which names the retired account_parties.save. build/audit_iam/survey_protocol.md
P06 Pass. party_cache is the component's only event cache. It warms through the canonical paged list request and subscribes to the canonical created, updated and deleted subjects, and its refdata producer is wired. It could not have been fed by iam's own events before the W01 fix. party_cache.hpp; party_cache_registrar.hpp
G01 Pass. Regeneration is byte-identical to the checked-in tree and idempotent for iam at every address: cmake, cpp, doc, plantuml, shell, sql, ts.domain and ts.protocol. Measured before any edit and after every model change. build/audit_iam/baseline_drift_iam.txt; the --dry-run runs after each edit
G02 Fix, with findings recorded. Deleted: the hand-written protocol umbrella (ten of twenty headers, no consumer), eight hand-written JSON I/O files beside the generated *_json_io twins, database_name_service (no caller tree-wide), and the superseded route includes that named the deleted headers. Recorded, not deleted: six generated *_table_io pairs with no consumer (generated, so removing them means disabling the facet in the model), and account_role_generator (test-only). commits "Delete the hand-written files the generation supersedes"; build/audit_iam/survey_hygiene.md
G03 Pass. 302 generated files carry one uniform marker naming their template; no hand-written file carries it; zero mismatches. build/audit_iam/survey_hygiene.md
G04 Findings recorded. The regenerated output was read and its shape matches the component's best code. Recorded, not fixed here because each is a house-wide template change: generated domain, eventing, protocol and handler types carry no export macro; the generator appends a uniqueness counter to every text code; and the generated account_party_repository::get_total_account_party_count_by_party has no caller. build/audit_iam/survey_hygiene.md; build/audit_iam/survey_symbols_coverage.md
G05 Fixes. Three generator defects are fixed at source with a regression test. The diagram capture read an operator function as a data member, because operator=( puts its equals sign before its parenthesis; the phantom member was committed in four components' diagrams, and the regression case fails on the pre-fix parser. The operation command-test template asserted CHECK(true); it asserts the menu's completion list against every declared command now, and all eight generated instances are regenerated. The FK-chain seed naming defect was fixed in the previous pass. commit "[codegen] Stop reading an operator function as a data member"; projects/ores.codegen/library/templates/ores.cpp.shell-command.operation_tests.org
G06 Pass. The iam CMake source lists match the tree after every deletion and addition; regenerate_cmake_component_files.py --all --check reports no iam staleness. build/audit_iam/cmake_check.txt
G07 Pass. No artefact was refreshed while a generator defect stood. The operator defect was fixed first, with its regression test, and only then were the four diagrams refreshed. The two-pass contract held in all four: the automated pass was re-run, the manual pass re-authored below the sentinel, and the sentinel verified present afterwards. Re-running the automated pass reports no changes, so it is idempotent. generate_component_puml.py dry runs; the four .puml files
G08 Fix. core and service already carried their outermost namespace headers; api and client did not and now do, with the namespace @brief the diagram conventions read, wired into the CMake lists. A latent defect found in passing: core/'s and =service/'s =ores.iam.service.hpp shared one include guard, so including both silently dropped one; each now has its own. projects/ores.iam/{api,client}/include/ores.iam.{api,client}/ores.iam.{api,client}.hpp; projects/ores.iam/{core,service}/include/…/ores.iam.service.hpp
W01 Fix, and the headline defect. The ten generated event registrars were never composed: the service created no postgres_event_source and opened no event_bus, so the ten notify channels the generated SQL triggers publish had no listener and iam's entity change events never reached NATS. Twelve other services wire theirs. Fixed by adding the aggregate composition point – the one hand-maintained line-per-entity call site that refdata and synthetic keep – and wiring it into application.cpp with start()=/=stop(). The drift gate cannot see this and the component's own eventing tests register their own mappings, which is exactly the trap the standard names. Proved against the running fleet, not the source: the service logs ten Registering canonical event mapping lines at startup, one per entity with its channel and subject prefix, and the live replay drove 19 events onto NATS, created and updated across accounts, account contact information, account types, login info, permissions, roles and sessions. The review must know: ores.assets, ores.workflow and ores.marketdata have the same unwired family. projects/ores.iam/service/src/messaging/event_registrar.cpp; service/src/app/application.cpp; build/audit_iam/verify_after_rebase.txt; the service log's mapping and publish lines
W02 Pass. check_handler_permissions.py is green with iam registered. build/audit_iam/check_handler_permissions.txt
W03 Pass. check_populate_references.py is green. build/audit_iam/check_populate_references.txt
W04 Pass. Schema validation reports 304 tables and 0 warnings, so no WIRE_001; every iam script is reachable from the create and drop aggregators, and the service grants cover what the service reads. build/audit_iam/validate_schemas_after.txt
S01 Partial. Coverage passes: 19 shell-enabled models, 123 commands, 0 uncovered verbs, every verb with a generated unit and a test. The deletion clause does not. Four hand-written units (accounts_commands, tenants_commands, rbac_commands, provision_commands) survive and are registered after the generated aggregator, so overlapping verbs are shadowed dead (accounts list, tenants get/add/delete, permissions list, roles list/get) while the rest duplicate generated behaviour under a second menu. Which verbs are superseded is a per-verb call-site question, and login has no generated unit, so the wholesale deletion this sentence asks for would remove live behaviour. The sibling ores.workflow task shows the correct granularity: retire the superseded registration, keep the verb generation cannot express. Recorded open with the mapping rather than half-done. build/audit_iam/survey_shell.md; projects/ores.shell/application/src/app/repl.cpp
S02 Fix, with two findings recorded. Every generated command has a literate recipe and the inventory lists all 210 documents; the stale account_history orphan (document, script and inventory entry, left behind when the model was deleted) is removed. Measured coverage against the code rather than assumed: the shell registers 205 commands and 123 of them are iam's generated ones, each with a generated recipe, so the generated surface is fully covered. Ten iam commands have no recipe – the top-level login and logout aliases, accounts sessions, accounts active-sessions, accounts sessions-for, accounts info, accounts history, accounts logout, tenants history and tenants complete-provisioning, all hand-written. Four of those lost their only recipe when the stale documents were deleted, which is the honest cost of the prune and the reason to rewrite them against current fixtures rather than restore the old ones. And the recipes cover the shell's surface, not the component's: the protocol is 151 subjects (121 request, 30 event) plus the C++ API, which the shell does not expose. Destructive commands: the operation models declare :destructive: true and carry the banner, but the entity projection never sets is_destructive, so a generated entity delete carries no warning – a generator gap recorded for its own change. doc/recipes/shell/shell.org; regenerate_shell_recipe_inventory.py --check; the command-versus-recipe census in build/audit_iam/; build/audit_iam/survey_shell.md
H01 Fix. The component passes the architecture audit. The automated pass was re-run for all four parts (api and client were already current; core and service refreshed), after the operator defect was fixed. The manual pass below the sentinel was authored in all four, because the automated pass reads data members and little else: 1116 hand-authored lines for api, 112 for client, 1547 for core and 262 for service, with the API filled into every box the automated pass left empty, the relationships it cannot infer drawn, and notes that state the semantics. Every rendered image was read, not merely regenerated. the four .puml=/.png= pairs; build/audit_iam/manual_ores.iam.{api,client,core,service}.md
H02 Fix, with items recorded. The dead code under G02 is deleted; the four hand-written convert_to_json copies collapse into the generated twins; the duplicate include guard is fixed; no commented-out code exists anywhere in the component. Recorded open: the four hand-written shell units duplicate generated behaviour (S01), and account_role, role_permission and auth_event have full hand-written stacks with no model behind them – a modelling gap, not dead code, since they are live. commits; build/audit_iam/survey_hygiene.md
H03 Fix. Twenty end-of-line comments and five edit-history comments are corrected, and the two template-owned ones are fixed at their source rather than in the generated file. No commented-out code was found. The last marker – a FIXME on signup's empty password_salt – is gone too: the stored hash embeds its own salt, so the comment now states that, and the vestigial column is recorded as work rather than left as a marker in the code. The template fix has one consequence outside the component: refdata's currency_pair_convention_cache_registrar.hpp is the template's only other committed output, and it carries the same comment move so that no committed artefact is left stale against its template. build/audit_iam/h03_comments.md; projects/ores.iam/core/src/service/signup_service.cpp; projects/ores.refdata/client/include/ores.refdata.client/service/cache/currency_pair_convention_cache_registrar.hpp
H04 Pass. No hand-rolled per-platform shim exists to replace: no _WIN32, __APPLE__, _MSC_VER, __GNUC__ or __clang__ guard, no ORES_NO_UNIQUE_ADDRESS and no hand-rolled unreachable(). Only include guards and BOOST_SYMBOL_EXPORT. build/audit_iam/survey_hygiene.md
H05 Fix. Every part carries a component_overview.org with the six sections, a Diagram section, a .puml and a committed rendered image, and the Diagram section points at the image. The composite root is a group-level index by the house convention, which the gate exempts and which every cleaned composite follows. Corrected rather than deferred: the api overview's claim to be header-only was false, and the messaging reference – which listed three subjects that do not exist and omitted 142 of the 151 live ones while being linked as the full catalogue – is now a family guide that names the generated headers as the catalogue and contains no subject that does not exist. the five overviews and their diagram pairs; projects/ores.iam/core/modeling/ores.iam.protocol.org
V01 Pass. The whole tree builds with zero errors, on the branch rebased onto origin/main 1a2e2054ea. An earlier run stopped at 79% because the base this branch started from – fb73fbd53b, which was main's tip when the branch was cut – did not carry ores.shell/workflow/tests/main.cpp, so that test target linked without a main. Main gained the file while this pass ran, and the rebase clears it; it was never an iam defect and this branch could not have fixed it. build/audit_iam/full_build_rebased.txt (0 errors); build/audit_iam/full_build.txt (the earlier failure, kept for the record)
V02 Pass. All four suites pass against the recreated database, twice: once on the pre-rebase tree (ores.iam.api.tests, ores.iam.core.tests 95.8s including the role eventing test this pass materialised, ores.iam.service.tests and ores.shell.iam.tests 165.3s) and again after the rebase (4 of 4). The codegen suite passes with 597 tests, including the diagram-parser regression case added here. build/audit_iam/ctest_iam.txt; build/audit_iam/verify_after_rebase.txt; build/audit_iam/codegen_suite.txt
V03 Pass on the rebased tree with no workaround at all. The database recreates from scratch, exit 0, and the fleet starts with 23 of 24 units running – ores.web.service needs Node, which this environment does not have. The blocker this row used to describe is gone because main removed the foreign key itself in the window this branch was rebased across, reading it better than I first did: the parent is temporal, so its only uniqueness on (tenant_id, id) is a partial index over the open row, and PostgreSQL refuses a partial index as a foreign key target. The locally neutralised statement this row carried is therefore moot, and the v2 verification on the rebased tree needed no workaround. validate_schemas.sh reports 304 tables and 0 warnings. build/audit_iam/ build_after_rebase3.txt and the recreate and fleet-start logs beside it
V04 Findings, not a pass, and an earlier version of this row overstated it. The generated surface replays as 123 recipes: 102 answered, 20 deliberately not replayed (the 18 generated entity delete=/=delete-many commands and the two declared-destructive resets), one aborts, and none is unwired. That one abort is a live defect, and it is now traced to its root cause, which is not in this component. ores.workflow's engine builds the workflow_instance and its first workflow_step without setting modified_by, and every table's insert trigger routes that field through ores_iam_validate_account_username_fn(), which raises 23502 – modified_by cannot be null or empty – once the tenant holds a non-service account. So the instance is never created (the live database holds zero workflow instances), and the poll can never succeed. iam's part is the misleading symptom rather than the cause: wait_for_workflow_instance() treats Workflow instance not found as a busy service and retries for 1500 seconds, so the caller's own timeout fires first and the operator sees NATS request timed out instead of the reason. The resolver's bootstrap branch is why this survived – an empty modified_by is silently defaulted while the tenant has no user accounts, so a bare database accepts the insert and only a bootstrapped one refuses it. Filed as capture C9A76A2E-E780-4457-8E60-096B96780398, then fixed surgically in this branch, because the whole workflow path was dead rather than merely slow: the four creation sites in the engine now set modified_by to the service account, and the hand-written foreign key that made the recreate impossible is dropped since the steps insert trigger already enforces the relationship as a soft foreign key. Verified against a database recreated with no workaround: 75 workflow instances and 183 steps, and this command returns success:true through all eight steps instead of timing out. The replay then measured 141 recipes with 121 answered and no aborts. That did not survive the rebase, and the honest count is one abort: on the rebased tree the same replay reports 120 answered and one abort, tenant_provisioning provision-acme-tenant – the command this row started with. It is no longer a wiring defect and no longer a stale document; it is a duration. The server completes all eight steps in about 32 seconds (21:31:46 to 21:32:19 in the service log, Step 8 included) while the shell's request timeout is 30 seconds – ores.nats' default_request_timeout – so the client gives up seconds before the answer arrives, and whether the recipe passes depends on machine load. Nothing in the call path can raise that timeout: the generated command passes none, the shell has no setting for one, and the model declares none. The handler's own Step 1 wait is 1500 seconds by design, so a single synchronous request under a 30-second default cannot carry this operation. The fixes are to make it asynchronous behind a poll, as the bundle publication it dispatches already is, or to let an operation command declare a longer timeout; both are design changes rather than surgical ones, and neither is done here. The two command defects this pass did fix remain fixed: the bootstrap form and tenants get-all. The fix surfaced one further defect it had been hiding, in ores.assets, recorded as its own capture. Two things this fix leaves open, both captured rather than glossed: the workflow records the service account as the actor instead of the caller behind the JWT, and iam's wait still cannot tell a failed start from a slow service. Earlier this row reported 158 recipes with 12 ANSWERED_WITH_ERROR and called it a pass; both were wrong. The 158 counted the 35 hand-written recipe documents that share the group directories beside the 123 generated scripts, and the harness treated Script aborted as the command answering, when an abort means the shell stopped at the first failing command and everything after it never ran. With an abort now its own failing status and the hand-written documents excluded by default, the honest total for the generated surface is 123 recipes: 102 answered, 1 aborted, 20 recorded. The stale hand-written documents are not merely recorded – they are deleted, seventeen of them with their scripts: the eleven that aborted, plus six more whose executable lines carry the same 2026-05 fixtures (accounts create, history, assign-role, revoke-role, permissions suggest) or duplicate a generated recipe (accounts bootstrap, whose command the generated bootstrap-create-initial-admin already covers). A fresh database refuses them because their fixtures – users newuser3, newuser40-42 and newuser60@example.com, the retired port 51004 – do not exist, and accounts create is non-idempotent besides: it re-creates fixed usernames, so it fails on its second run whatever the environment. The group directories now hold 141 recipes: 120 answered, one aborted (the generated provisioning command above), 20 recorded. Two real wiring defects were found and fixed at source earlier: the bootstrap recipe called bootstrap <principal> <password> <email>, a form the CLI has never known, and tenants get-all, a verb retired in favour of tenants get-many. The harness is generalised (groups, evidence directory, skip list, login command, password quoting) and now distinguishes an abort. .audit/clean-iam-gen/A30_v04_recipes.tsv (generated), .audit/clean-iam-all/A30_v04_recipes.tsv (with hand-written), build/audit_iam/v04_honest.txt; check_shell_recipes.py
V05 Pass. Every codegen gate is green with iam registered: model drift, protocol twin coverage, handler permissions, populate references, the shell recipe inventory, and the CMake source lists. validate_docs.sh reports no iam violation; its two violations are ores.shell.workflow, which belongs to another task's part. build/audit_iam/{check_model_drift,check_protocol_twin_coverage,check_handler_permissions,check_populate_references,recipe_inventory_check,cmake_check,validate_docs}.txt
V06 Decision, for the review. iam has been listed in COMPONENTS_UNDER_TEST since sprint 25, and check_component_drift.py --all passes with it listed. The standard keeps a component out until every item passes or its exceptions are accepted, and this pass found items that are open rather than passed. The entry is kept, because withdrawing it would switch the gates off for a 429-file component, and every open item is recorded here rather than hidden. The review decides: accept the open items as exceptions, or withdraw the entry until they close. component_registry.py; build/audit_iam/baseline_drift_all.txt
V07 Fix. The generated entity command tests are non-vacuous: they assert the menu's completion list against the derived verbs. The generated operation command tests were not – their registration case asserted CHECK(true). The template is fixed and all eight instances regenerated, so every declared operation command is now asserted against the menu's completion list, which is the only public view of its children. An earlier draft of this row recorded the item as house-wide on the assumption that other components carry operation units; the census says otherwise – all eight generated operation test files are iam's, and no other component has one – so the defect was this component's and is fixed here. Non-vacuity was proved by stubbing: with register_commands stubbed to do nothing, the suite reports one failed case and exactly three failed assertions – the three bootstrap commands it declares – while the other 244 cases pass. projects/ores.codegen/library/templates/ores.cpp.shell-command.operation_tests.org; projects/ores.shell/iam/tests/*_operations_commands_tests.cpp; build/audit_iam/v07_ctest.txt; build/audit_iam/v07_stub_ctest.txt
V08 Findings recorded. 151 source files: 87 touched by a test, 64 untouched. Untouched by directory – api/src 12 (11 *_table_io.cpp and auth_session_service.cpp); core/src 38 (19 messaging registrars and dispatch, 8 presentation history mappers, 9 services, auth_event_repository.cpp, token_settings.cpp); service/src 13 (main.cpp, application.cpp, the 11 event registrars); client/src 1 (the part has no test target at all). The method was static include and symbol reachability, so "touched" is not branch coverage and a NATS-only runtime path would be missed. This list is the coverage work item. build/audit_iam/survey_symbols_coverage.md; build/audit_iam/v08.py

5. Notes

  • The component is not "more or less clean", and the record should say so. It is in the registry as the standard's reference component, and the pass found: ten generated event registrars with no caller, so no entity change event ever left the component; five entities added after the sprint-25 bind and never bound; a hand-written JSON I/O layer beside its generated twin; a dead protocol umbrella; a dead service; a boolean flag stored as an integer in three columns, one of which carried a partial-index predicate that would have failed outright once the type was corrected; and a messaging reference that named subjects which do not exist. The sprint-25 survey bound the six orgs that existed then and was correct about them; the debt is what landed after it.
  • The most useful single check was not a gate. W01 – the unwired event family – is invisible to codegen drift, because regeneration produces the files either way, and invisible to ctest, because the generated eventing tests register their own mappings. Only reading the composition points found it. The standard says as much; this component is the proof.
  • The items left open, and why. Each is named above; the shape of the reason matters. S01's wholesale deletion would remove live behaviour, so it needs the per-verb analysis the workflow task did. The six dead *_table_io pairs and the missing export macro are generated from templates shared with every component, so fixing them here would rewrite the estate; the vacuous operation test template looked like the same case and is not – its eight instances are all iam's, so it is fixed here. The unmodelled tables (account_role, role_permission, auth_event) are new modelling work, not cleanup. The account write protocol and the list_by_as_of flag are design decisions. The permission_codes constants need adoption by the components that hardcode the literals. V08's uncovered list is a coverage work item. B06, M02, P02, P03, P04, P05, G02, G04 and S02 each close with findings recorded rather than an exception taken, so the review can weigh them.
  • One earlier claim on this task was wrong and is corrected here. The vacuous operation-test template was recorded as house-wide on the assumption that other components generate operation command units. The census says no other component has one: all eight generated operation test files are iam's. The item was therefore this component's, and is fixed rather than deferred.
  • House-wide findings this pass produced, beyond the component. ores.workflow's workflow_constraints_create.sql adds a foreign key to a non-unique key, so compass db recreate aborts on main – a blocker for every component's V03, not only this one, and the reason V03 here carries a locally neutralised statement rather than an unqualified pass. The event-mapping family is unwired in ores.assets, ores.workflow and ores.marketdata as well. The operator-parse defect left phantom members in ores.synthetic and ores.testing diagrams, which a refresh clears. ores.variability declares system_setting_changed and never publishes it, so iam's and ores.http's subscriptions to it are dead. ores.web duplicates ten iam subjects and their zod bodies by hand. The application_exception class is never thrown in any of the twelve services that include it.
  • The standard grew while this pass ran, and iam is checked against the growth. Between the base this branch started from and the main it rebased onto, component_clean_standard.org gained lessons from the ores.scheduler pass. Two of them imply a check here. The first is that a unique index on a nullable tenant does not make NULL one tenant: a tenant-scoped natural key on a nullable tenant_id still admits duplicate system-scope rows, because PostgreSQL treats NULLs as distinct unless the index says nulls not distinct. Measured against the live database rather than the generated text: the iam tables carry 103 columns across their unique indexes, and not one of them is nullable (pg_index joined to pg_attribute), so the failure mode cannot occur here. The second is that model prose becomes the generated documentation, so a model states facts and a task states instructions – which is the same defect the M09 row records and fixes for role and session, and the reason the generator snippet's rationale in account_contact_information was moved into the column prose. The remaining lessons (weld-free decisions, an unlinkable class as a finding, re-measuring cited figures, a private constructor as an invariant) are already the shape of this record's V07, V08 and M02 rows.
  • Verification evidence is under build/audit_iam/ (survey reports, gate outputs, drift baselines, bind tests, the manual-pass reports). The build/ tree is not committed.

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
#2192 [doc] Capture the two iam follow-ups that lived only on the task
#2180 [iam] Bring ores.iam to the component clean standard

8. Review

Comment summary File Decision Notes
       

9. Result

Merged as PR #2180, 19 commits, every check green. The task stays STARTED, deliberately: the standard keeps a component out of the registry until every item passes or its exceptions are accepted, and V06 puts that decision to the review rather than to me.

What landed. Fifteen defects fixed at source. The one that mattered is W01: the component published no entity change events at all, because the ten generated registrars had no composition point – invisible to the drift gate and to ctest alike, and proved fixed against the running fleet by 19 events on NATS. Then the last two entities bound to profiles, with two no-matches recorded rather than forced; login_info's three flags moved from integer to boolean with every predicate and writer that had to follow; a hand-written JSON I/O layer, a protocol umbrella, a dead service and seventeen stale recipe documents deleted; the diagram capture's operator-function defect fixed with a regression test that fails on the pre-fix parser, and all four diagrams refreshed with ~3000 hand-authored lines read back as images; the operation command tests made to assert the menu instead of CHECK(true), proved by stubbing; and documentation that was materially false corrected.

Verified. The whole tree builds. All four iam suites and all three workflow suites pass. Registration is byte-identical and idempotent at every address. Every codegen gate is green with iam registered, including the new registry check. Schema validation reports 304 tables and 0 warnings. The database recreates from scratch with no workaround, because main removed the foreign key that had blocked it. 141 shell recipes replay live: 120 answered, 20 recorded as destructive, one aborts.

What did not land, and why. One recipe still aborts, and the reason is a duration rather than a defect in the command: tenant_provisioning provision-acme-tenant takes about 32 seconds on the server while the shell's request timeout is 30, so the client gives up seconds before the answer arrives. Both fixes are design changes – make the operation asynchronous behind a poll, or let an operation command declare a longer timeout – and neither is surgical. The items the review must weigh are otherwise recorded individually in the table above: S01's hand-written shell units, the six dead *_table_io pairs and the missing export macro, the unmodelled account_role=/=role_permission=/=auth_event tables, and three design decisions. Three defects this pass found in other components are filed as captures: the workflow start failure (fixed here), the assets publish function's missing columns, and the workflow actor attribution the fix leaves to its owner. Two of this task's own follow-ups are captured beside them, so they do not live only in the table above: the four iam commands left without a recipe when the stale documents were deleted, and the provisioning command's duration against the client's request timeout.

Emacs 29.3 (Org mode 9.6.15)