Shell facet

Table of Contents

A model's shell commands are codegen output. The ores.cpp.shell-command facet emits one command unit per entity, junction or operation model: a submenu named for the model's plural, and one command per verb or per declared message. The unit lands in the adapter part named for the model's component.

Return to Knowledge.

1. The facet

Field Value
Facet ores.cpp.shell-command
Model type domain_entity, junction, operation
Level cross
Default disabled
Archetypes command_header, command_implementation, command_tests, operation_header, operation_implementation, operation_tests

Each model owns its own three files. Nothing is aggregated into one file, so a unit can be added or removed without touching its neighbours.

2. How a model opts in

The facet is off unless something turns it on, and three things can:

  1. The facet node's own #+default:. It is disabled today.
  2. A variability profile's * Physical space table. A row | =ores.cpp.shell-command | true |= turns the facet on for every model bound to that profile. Seven profiles fix it: fk-scoped-child, fully-featured-lookup, self-referencing-hierarchy, simple-lookup, tenant-scoped-junction, uuid-identified-lookup and uuid-surrogate-lookup.
  3. The model's own properties drawer, :ores.cpp.shell-command.enabled: true.

A model whose profile already fixes the facet states nothing itself. The drawer entry exists for the models that bind no profile, which is every operation model and the entities that carry no :profile: at all. Every entry in the shell surface generation story was checked against this rule, and the thirteen entries a profile already covered were removed.

3. What withholds a unit

Two gates run before the opt-in is read, so neither can be overridden by a drawer entry:

  • A staging shim whose plural another component also models renders no unit. The shell names its menu after the plural, so two owners of one plural would answer the same menu, and the shell resolves a duplicate by registration order. dq's report_definition shim is withheld from reporting's report_definitions menu this way.
  • A model that suppresses ores.cpp.protocol renders no unit, because the unit includes the derived request types that do not exist without it.

4. The entity and junction unit

The submenu is named for the model's plural. The verbs are the ones the shape derives:

Verb Shape
list One page, with --offset, --limit, --order and --desc.
get One row by key.
get-many Rows by a list of keys.
add A create.
set A replace, with --version.
put-many A batch upsert.
delete A delete, with --version.
delete-many A batch delete.
versions The version history, paged.
version One version, with --version.

A junction derives the same set, and a parent-scoped child adds its by-<parent>-id read. A current_state entity has no version rows, so its unit carries no versions or version.

The add verb takes free-form tokens rather than one typed parameter per column. The cli library expands a typed handler into one template instantiation per argument, and the Windows clang toolchain abandons the mangler on the deeper chains. A token vector holds the argument list at a fixed shape.

5. The operation unit

An operation model states messages, so its unit carries one command per message that declares a subject and a response. The command name is the message name without its _request suffix, kebab-cased, unless the message states a shell_command of its own.

The body sends the request and prints the reply through its JSON codec. An operation unit therefore reads as JSON where a hand-written command may have printed rows.

A request field with a :default: arrives as a --<name> flag and the struct's own initialiser stands when the caller omits it. A field without one is a positional argument, so a request model that means a filter to be optional states a default for it.

6. Who supplies a column's value

The add verb fills one domain member per positional argument. The :supplied_by: annotation on a column says who supplies the value.

Value Meaning
user The default. The verb reads the value from the next positional.
minted The verb mints a fresh uuid. The default for a uuid primary key.
session_party The verb reads the acting party from the session, and fails when the account has no default party.
unset The verb leaves the member at its default and asks for nothing.

The tenant_id member takes no annotation. It is not a declared column, so the template names it directly and fills it from the session.

7. File locations

The unit belongs to the adapter part named for its component, not to ores.shell.application.

File Path
Header projects/ores.shell/{component}/include/ores.shell/app/commands/{component}/{entity}_commands.hpp
Implementation projects/ores.shell/{component}/src/app/commands/{component}/{entity}_commands.cpp
Tests projects/ores.shell/{component}/tests/{entity}_commands_tests.cpp

An operation's three files are named {operation}_operations_commands.

Every consumer includes the header as ores.shell/app/commands/{component}/{entity}_commands.hpp, which is its path under the part's own include/ directory. The part declares #+component_kind: adapter, so codegen writes only its two file lists beside the hand-authored CMakeLists.

8. Registration

One aggregator per component wires the units to the shell host. It is hand-written and lives in the application part at projects/ores.shell/application/src/app/commands/{component}/{component}_commands.cpp. It includes each unit's header and calls register_commands once per unit. repl::setup_menus calls the aggregator once at startup.

The facet has no component-scoped archetype for that aggregator, so adding a model means one include and one call in the aggregator, plus a refresh of the part's file lists. A generated aggregator needs a render-time list of the component's opted-in models, the same pattern the generator already uses for _COMPONENT_FILES_TEMPLATES.

The aggregator registers no command of its own, which is why it is not counted as a hand-written command unit.

9. The units replace, they do not join

Two units must never register one submenu. The shell resolves a duplicate name by registration order, so the first unit answers every verb the two share and the second unit's verbs answer without appearing in the help.

Delete the hand-written unit for a capability in the same change that generates its replacement. A hand-written verb that a generated one supersedes is a defect, not a compatibility layer. Where a hand-written command stays, it is because the model cannot state what it does: it changes this client's own session, it dispatches a workflow and waits on it, it composes several responses into a view no model declares, or it moves a file.

10. Enabling the facet by default

The facet's default is the last lever, and it is not yet safe to pull. Flipping #+default: to enabled would render a unit for every model of every component, not only the components that have been cleaned.

Measured on 2026-10-01 at 4eec0e6740: 76 models across 16 components would gain a unit. Nine of them bind one of three profiles that do not fix the facet (readonly-timeseries, table-less-record, trading-instrument); the other 67 carry no :profile: at all and are operations or entities whose component has not finished its clean-standard pass.

The precondition is therefore that every component reaches the clean standard first. Until then, a model opts in from the profile that covers it, or from its own drawer when no profile does.

11. See also

Emacs 29.3 (Org mode 9.6.15)