The entity specification (superseded)

Table of Contents

This page is superseded. Do not act on it.

The entity-screen approach is abandoned. This page specified one shared list screen, one shared detail screen and one shared history screen, driven by a generated entity declaration. All of that is deleted: the screens, the shared DataTable and FieldControl controls, the entity declaration and its generated UI metadata, the component registry, the routes, and scripts/verify-country.ts. The interface is to be built from hand-written journey screens instead, and a journey is not entity-shaped.

Nothing here is a checklist now. Every "read this before writing entity code" instruction below, and every requirement in it, is void.

Screens that list and maintain records now follow the Record screen standard, which keeps this page's scope without the generated entity screens.

Read component_specification.org for the current specification of the component: the shell, the session, the transport to the BFF, the i18n catalogue, and the wire-protocol package.

The original specification follows unchanged as history.


What it means to be an entity in this interface, written so that implementing entity number one hundred is the same process as entity number one.

Read this before writing entity code. When asked to implement an entity, this document is the checklist.

Where a component and its entities sit in the navigation, and what a component declaration contains, is component_specification.org. This document covers the screens themselves.

The audit of this specification is gaps.org: it judged every requirement against the reference entity. It is superseded for the same reason as this page.

What this document is. It is derived from the Qt client, which already carries the accumulated decisions, but it is not a transcription of it. The Qt client contains two archetypes and one is better than the other. Where they disagree this document states the rule and names the divergence, so that "what should this screen do" has one answer rather than two.

1. Two archetypes, and which one is the standard

The generated standard. Produced by codegen from the entity's .org model into Client<Entity>Model and <Entity>MdiWindow. This is the pattern that repeats for hundreds of entities: single selection, a standard toolbar, one tab per field group, and a change reason prompted on every create, amend and delete. ClientCountryModel and CountryMdiWindow are the reference.

The legacy hand-written screens. AccountMdiWindow, ClientAccountModel, AccountDetailDialog. Older, richer in places, inconsistent in ways that matter. Multi-selection, a per-entity delegate, no column metadata, six bespoke tabs.

The accounts screen is the one the request named, and it is a good reference for what an entity screen contains. It is not the standard for how it behaves, because it deviates in specific and important ways listed in section 13. This document follows the generated standard where they disagree.

2. Vocabulary

Fixed terms, used consistently in code, in the interface, and in conversation.

Term Meaning
Entity A domain record with identity, a version history, and an audit trail
Collection The plural noun. The entity is book, the collection is books
List screen The table of an entity's records with its toolbar and paging
Detail screen The form for reading, creating, editing and deleting one record
History screen The read-only list of every version of one record
Reference data A short, stable set that other entities point at
Transactional data Records that accumulate and are rarely edited
Lookup entity A reference-data entity with a natural code, such as GBP
Child entity A record that exists only within a parent
Junction entity A record joining two entities
Temporal A record carrying validity dates, so the past can be reconstructed
Audit fields The who and why: reason code, commentary, and the actors
Provenance What wrote the record and when: version, recorded time, source
Field group A named set of fields that becomes one tab or one section

Naming in code follows section 12. Never invent a synonym.

3. What makes a record an entity

An entity has all six of these. If it lacks one it is not a regular entity and needs a deliberate exception rather than a shortcut.

  1. Identity. A stable primary key, almost always a UUID. The key never changes. Some lookup entities key on a short natural code instead.
  2. A display name. The field a person recognises the record by, unique within its scope.
  3. A version. An integer incremented on every accepted change, used for optimistic locking.
  4. An audit trail. Who made the change and why, on every accepted write.
  5. Tenancy. A tenant identifier, even where only one tenant exists, so the scoping rule is uniform.
  6. A lifecycle. Created, read, amended, deleted. Deletion is soft where anything references the record.

3.1. Properties every entity carries

Property Type Purpose
id UUID Identity
version integer Optimistic locking. Increments per accepted write
tenant_id UUID Tenancy scope
change_reason_code string Why the change happened
change_commentary string Free text, may be empty
modified_by string The account whose change produced this version
performed_by string The account that asked for it. Differs from modified_by when a service acts for a person
recorded_at timestamp When this version was written

3.2. Properties some entities carry

Property When
valid_from, valid_to Temporal entities
A parent reference Entities in a hierarchy
image_id Entities that show a picture
A natural code Lookup entities
A status Entities with a lifecycle beyond existing

4. The layers

Five layers, each with one job. Every entity has all five.

Screen        list, detail, history                React
Query         server state, cache, mutations       React Query
Endpoint      HTTP, validated at the boundary      BFF
Protocol      subjects, request and reply shapes   @ores/wire-protocol

Screen. Presentation only. No fetching, no business rules, no knowledge of subjects or wire field names.

Query. One hook per read, one mutation per write. Owns caching, invalidation, and the loading and error states the screen renders.

Endpoint. Validates input, calls the protocol layer, maps failures to status codes. The only layer above the protocol that knows anything about it.

Protocol. Subjects, request and reply schemas, wire encoding. The only layer that knows a wire field name.

The Qt client reached the same conclusion with the same shape: the four-layer stack of controller, list window, detail dialog and client model, documented in its own entity_controller_pattern.org. The layers differ in language, not in responsibility.

4.1. What that buys

Adding an entity touches the protocol for its operations, the endpoint for its routes, and the screens. It never touches another entity's code. If adding an entity requires editing shared, unrelated code, that shared code is missing an abstraction and fixing it is the first task.

5. The screens

Screen Route Purpose
List /<collection> Browse, filter, select, act
Detail /<collection>/:id Read one record
Detail, new /<collection>/new Create one record
Detail, edit /<collection>/:id/edit Amend one record
History /<collection>/:id/history Every version of one record

Read and edit share the detail screen because they render the same fields. The mode changes which are editable and which actions appear, not the layout.

6. The list screen

<Plural>                                        [Refresh] [Add]
A sentence saying what this collection is.
[ Search...          ]  [ Type v ]                       42 of 380
---------------------------------------------------------------------
 Name         Type      Code       Status     Modified    Recorded
---------------------------------------------------------------------
 ...                                                             ...
---------------------------------------------------------------------
No records           < First < Prev  Page 1 of 4  Next > Last >
                     Page size: [100 v]  [Load all]

6.1. Header

  • The plural noun as the page title, sentence case.
  • One sentence saying what the collection is.
  • Actions at the right: Refresh and Add (primary).

6.2. Toolbar and row actions

The Qt standard's action set, in its order, with its icons:

Action Icon Enabled when Behaviour
Refresh arrow_clockwise Not loading Reloads from the first page
Add add Always Opens the detail screen in create mode
Edit edit Exactly one row selected Opens the detail screen in edit mode
Delete delete One or more rows selected Confirms, then deletes
History history Exactly one row selected Opens the history screen

An entity may add its own actions after History. The accounts screen adds Lock, Unlock and Reset password there, which is the right place for an action that acts on the selection.

Opening a record is a click on the row. Edit exists for discoverability, not as the only way in.

The Qt client has no keyboard shortcuts. There should be here. Enter opens the selected record, Delete removes it, and the table is reachable from the keyboard. The omission is a gap, not a convention worth carrying.

6.3. The table

  • Columns come from a single declaration per entity, which is also what the renderer uses. One source, so a column cannot be styled differently from how it was declared.
  • The Qt client has ten column styles and they are worth keeping, because they capture real distinctions: text_left, text_center, mono_left, mono_center, mono_bold_left, mono_right, mono_bold_center, icon_centered, icon_text_left, badge_centered.
  • The display name is the first column and is the row's identity to a person.
  • Selection is single. The accounts screen uses multi-selection, which is a legacy deviation; bulk operations are a separate feature with a separate design, not a table mode.
  • Audit columns are hidden by default and available through the column menu; the set is in section 12.
  • Column visibility and order are remembered per user, and the saved state is versioned so a change to the column set does not restore an incompatible layout.
  • Sorting is available on every column. The default is the entity's natural order: name for reference data, most recent first for transactional data.

Two corrections to the Qt behaviour, both deliberate. The Qt client sorts on the displayed string, so a numeric column sorts lexically and 10 comes before 9; here, sorting uses the underlying value. And the Qt client declares a default column width that no code consumes, so columns are whatever the user last dragged them to; here the declaration is honoured.

6.4. Footer

Paging, with the count first, then First, Previous, Next, Last and Load all, then "Page size:" with a selector offering 25, 50, 100, 200 and 500, defaulting to

  1. That is the Qt client's shape and it is kept.

Load all is offered only when the total is at or below 1000, which is the rule the Qt client already applies. Offering it for a hundred thousand records is a trap.

Changing the page size or page number resets to the first page, and a reload after a save returns to the first page rather than the page you were on. The Qt client documents this and it is right: the changed record may no longer be on the page you were looking at.

6.5. States

State Presentation
Loading, first time A quiet "Loading…". Never an empty table with no explanation
Loading, with data Keep the rows, dim them, mark the count as refreshing. Never blank the table
Empty, no records Say the collection is empty and offer Add. The Qt client shows "No records" in the paging label alone, which is too quiet
Empty, filtered "No <collection> match the current filter." with a way to clear it
Error The server's own words, and a Retry. The Qt client uses a modal box; a screen-level message is better because it does not steal focus and can be retried in place
Stale The Refresh action pulses

6.6. Recency

The Qt client has a feature worth keeping and worth explaining, because it is invisible until it fires. After a reload, rows whose recorded_at is newer than the time of the previous reload are highlighted, and the highlight pulses to draw the eye. On the first load there is no baseline, so nothing is highlighted.

The colour is a single gold, rgb(255, 215, 0), applied to the text rather than the background. The pulse runs six cycles at 500 ms. The point is that a person watching a live system sees what just changed without re-reading the table.

Worth carrying across, and worth making a setting, because a table that flashes on every update is worse than one that never does.

7. The detail screen

7.1. Layout

The Qt dialog is 600 by 700 with a minimum of 600 by 500. The root is a tab widget above a button row. The button row, in order, with its icons, is Delete (delete), Close (dismiss), Save (save), with Save as the default action.

A read-only toolbar appears above the tabs only when viewing a historical version, offering Revert to this version.

<Name of the record>                              [Save]
[ General ] [ <Field group> ] [ Related ] [ Provenance ]
 Field                Field
 Label                Label
 [______________]     [______________]
                                      [Delete]  [Close]

7.2. Tabs, and when each applies

The generated standard emits one tab per field group declared for the entity, plus Provenance. The accounts dialog instead has six fixed tabs. The field-group rule is the standard because it is derivable from the model; the account's fixed tabs are not.

Tab Applies to Contents
General Every entity The entity's own fields. Always first, always present
<Field group> When the entity declares one The fields in that group
Related Entities with child or junction records One section per relationship, each a small table with its own Add and Delete
Provenance Every entity Read-only. The six rows in section 7

A tab that would be empty is not rendered.

Fields with nothing to show display "not recorded", so a blank is never mistaken for a failure to load.

7.3. The form

  • One label per field, above the control. The Qt labels carry a trailing colon, "Username:". Sentence case without the colon is the web convention and reads the same; this is a deliberate departure.
  • A hint below the control when the field needs explaining.
  • Required fields are marked, and the mark is described in words for a screen reader rather than being a bare asterisk.
  • Read-only fields are visibly read-only. The Qt client disables them in some places and sets them read-only in others, inconsistently; read-only is the rule, and a disabled control is used only where a whole section is inactive.
  • The key field is editable only when creating. The Qt rule is exactly right and applies to every entity: setReadOnly(!createMode). The username, the code, the natural key, is fixed once the record exists.
  • The type or classification field is likewise disabled after creation, because changing what a record is changes what it means.
  • Validation runs on blur, never on every keystroke, and never clears a message while the person is still typing the fix.
  • A field with an error shows the message beneath it, and focus moves to the first field with an error on a failed submit. The Qt client has no inline errors at all and uses a modal warning; inline is better and is the standard here.

7.4. Field controls

The Qt model declares a widget type per field, and the set is good because each implies its validation:

Type Control Notes
line_edit Text input The common case
text_edit Text area, 80-150px Commentary and notes
static_combo Select, fixed options The options are declared, not fetched
dynamic_combo Select, options fetched A foreign key, loaded from its collection
flagged_combo Select with flags Countries and similar
check_box Checkbox Optionally tri-state, which needs a "not set" state distinct from false
spin_box Number input With a minimum and maximum; a nullable integer shows "(unset)"
colour Colour swatch Opens a picker
date Date picker Validated as a real date before parsing

A field that is a foreign key is always a searchable select, never free text. Typos in keys are the most expensive kind of bad data.

7.5. Actions

Action Placement Behaviour
Save Primary, top right Validates, prompts for the audit reason, submits
Delete Danger, bottom right Confirms, prompts for the audit reason, deletes
Close Secondary, bottom right Leaves, confirming if anything changed

Delete is not rendered in create mode. The Qt client disables it rather than hiding it; hidden is better, for the reason in section 11.

7.6. Unsaved changes

Closing with unsaved changes prompts: title "Unsaved Changes", text "You have unsaved changes. Close anyway?". That wording is exact and worth keeping.

8. The audit rule

Every accepted write carries a reason. This section is the one place the accounts screen is not merely different but wrong, and the difference is worth being explicit about.

8.1. What the accounts screen does

  • Creating an account prompts for no reason, and save_account_request has no audit fields to carry one.
  • Deleting an account prompts for no reason.
  • Editing an account prompts only when parties or roles changed. A change to the email, the full name or the job title is sent with empty audit fields, even though update_account_request has the fields.

So a person can change whose email an account uses, and the record will not say why. That is a defect, not a convention.

8.2. Server-owned fields must still be well formed

The service overwrites what it owns: the tenant, the actor, the timestamp, and the version. That is a security boundary and it means a client cannot forge them.

It does not mean a client may omit them or send an empty string. The request is decoded before any of the service's checks run, so a field that does not parse fails the whole request, and it fails as an unexplained refusal rather than as a validation error. A create, which has no tenant and no timestamp, must therefore send a tenant and a timestamp anyway — placeholders that parse, which the service then replaces.

This is worth stating because the failure mode is expensive: a missing field and a refused permission look identical from the client, and the difference is days of work.

8.3. The rule

Every create, amend and delete prompts for a reason, and the reason is stored. One dialog serves all three, and its wording follows the operation:

Operation Title Prompt Commit button
Create New Record Reason Please select a reason for creating this record: Create
Amend Change Reason Required Please select a reason for this change: Save
Delete Deletion Reason Required Please select a reason for this deletion: Confirm Delete

The dialog is at least 450px wide, offers the reason codes the server provides, and shows the selected reason's description beneath the selector in muted italic. The commit button is disabled until a reason is chosen.

8.4. Reasons are filtered by whether anything actually changed

This is the rule that matters most and is easiest to get wrong. One code, common.non_material_update, means "touched but nothing changed". For a create every reason is offered. For an amend or a delete:

  • If fields were modified, the non-material reason is disabled, with the tooltip "Not available when fields have been modified".
  • If no fields were modified, every other reason is disabled, with the tooltip "Only available when fields have been modified".

So a person cannot record a material change as a touch, nor a touch as a material change. The enabled set is a function of the diff, computed rather than trusted.

8.5. Commentary

Some reason codes require an explanation. The label then reads "Commentary is required for this reason." and the field is marked required. Otherwise it reads "Commentary is optional for this reason."

8.6. Reason categories

Reasons belong to a category, and the entity's category selects which are offered: system for creation, common for amendment and deletion. The categories come from the server, not from a list in the interface.

8.7. Provenance

The detail screen's Provenance tab is read-only and shows exactly six rows, in this order, with these labels:

Version, Modified By, Performed By, Recorded At, Change Reason, Commentary

The tab is disabled in create mode, because a record that does not exist has no provenance.

9. The history screen

Two layouts exist in the Qt client. The generic compare-mode dialog is the standard; the accounts screen's bespoke one is not.

9.1. The standard

A horizontal split. On the left a timeline, on the right a diff.

  • The timeline is 220-320px wide, headed "History Timeline", newest first. Each entry reads v<version> . <relative time>, with the full timestamp on hover, and below it the modifier, the performer, the reason code as a badge, and the commentary in italic. Clicking previews that version against its predecessor.
  • The right pane is headed "Compare:", with two selects for the from and to versions and a control offering "All Fields" or "Only Changes". The diff uses line and span highlighting, so a changed span within a line is marked rather than only whole lines.
  • Choosing a from version newer than the to version says "The From version must be older than the To version." Selecting the same version twice says "(No field changes)".
  • The oldest version has nothing to compare against and shows "(Initial version)". A version with no differences shows "(No field changes)".
  • Provenance fields are excluded from the diff and shown on the timeline entry only, because version and actor differ by definition and diffing them is noise.
  • The toolbar offers Reload (arrow_clockwise), Open (edit) and Revert (arrow_rotate_counterclockwise). Open can be used with any selection; Revert only when the selection is not the current version.
  • Reverting prompts: "Are you sure you want to revert '%1' from version %2 back to version %3? This will create a new version with the data from version %3." Reverting creates a new version; it never removes one.

9.2. The accounts shortcut

Opening a version in read-only mode reopens the detail screen with Save and Delete hidden, a "Revert to this version" action shown, and the Security tab disabled. That is the right treatment and is kept.

10. Child and related records

A child entity is never listed independently. It appears as a tab on its parent with a table and two actions.

The generic contract is a table built from column headers, an add label and a delete label. For an account's contact details the columns are Email, Country, Street, City, Phone, and the actions are Add Contact and Delete Contact.

The rules, all of which the Qt client gets right and are worth keeping:

  • The table is read-only; double-click opens a child dialog. No inline editing.
  • Adding before the parent is saved is refused with "Save the account first, then add contact information."
  • Adding while disconnected is refused with "Cannot add contact while disconnected from server."
  • Deleting asks for confirmation. The Qt client does not prompt for a change reason here, which contradicts section 7 and is treated as a defect.
  • The whole section is read-only when the parent is being viewed historically.
  • The column set is declared once per relationship, never hand-built inline.

11. Icons

Every icon comes from Microsoft Fluent UI System Icons, named ic_fluent_<concept>_<size>_<variant>. Regular variants are for passive and secondary actions, filled for active and primary. Sizes are 16px inline and in dense tables, 20px in buttons and navigation, 32px in headers, 48px for empty states. The application recolours icons to rgb(220, 220, 220) by default.

The full vocabulary is in icon_reference.org, which lists all eighty concepts in twelve categories with their file names, and the process for adding one. The table below is the entity-facing subset: what an entity screen needs. Use it, and when an entity needs an action it does not list, add the concept to both tables rather than choosing freely at the call site. Two entities using different icons for one idea is the inconsistency these documents exist to prevent.

11.1. Actions

Concept Icon Used for
Refresh arrow_clockwise Reloading a list; the stale pulse animates this same icon
Add add Creating a record
Edit edit Opening a record, and opening a historical version
Delete delete Removing a record
History history Opening the version history
Save save Committing a change
Close dismiss Closing without saving
Revert arrow_rotate_counterclockwise Restoring a historical version
Undo arrow_undo Reverting an edit
Redo arrow_redo Reapplying an edit
Copy copy Duplicating a record
Search search Search fields
Filter filter Filter controls
Settings settings Configuration
Confirm checkmark Confirmation, selected state
Star star Favourited, filled variant for the active state
Generate wand Generating synthetic or sample data
Import arrow_download Reading a file
Export arrow_upload Writing a file
Purge delete_dismiss Bulk destruction, not ordinary deletion
Publish publish Uploading to production tables
Lock lock_closed Locking a record
Unlock lock_open Unlocking a record
Password reset password_reset Forcing a password change
Terminal terminal Shell and console surfaces
Record record Recording controls

A dropdown chevron is not from this set; it is the platform's own control.

11.2. Cells

Concept Treatment
A flag column A 16px flag, centred when alone, followed by text when combined
Boolean true The word "Yes", or a checkmark where space is tight
Boolean false The word "No", or a dismiss mark
A status or classification A pill, coloured by meaning, text only
An identifier Monospace, left aligned
A quantity or amount Monospace, right aligned, tabular figures
A timestamp Monospace, relative, with the exact value on hover
A currency Its code, monospace
An unmapped lookup value The fallback pill, orange with a dashed border, so a missing mapping is visible rather than silent

An icon never appears alone where its meaning is not obvious. If removing an icon loses no meaning, remove it.

12. Feedback, access, and safety

12.1. Feedback

Situation Presentation
A save succeeded A brief confirmation naming the record, then return to the list
A save failed validation Field messages where the problem is, and a summary at the top
A save failed on the server The server's own message, verbatim, because it knew something we did not
A name is already taken The conflict names the name, and the field is highlighted
A session expired A clear statement, and a way to sign in again. Never a silent failure
The server is unreachable Said plainly, with a Retry. Never a spinner that never resolves
The record changed underneath Said so, and offer to reload. The Qt client marks the list stale and shows "has been modified on the server"

Every message says what happened and what to do next. "Error" alone is not a message. The Qt client's exact validation strings are worth keeping: "Username is required.", "Passwords do not match.". The server's own text is always preferred where there is any.

12.2. Access

  • An action the person may not perform is not rendered, rather than rendered and disabled. A disabled button invites a question a missing one does not.
  • Permissions are decided by the server. The interface hides what the server would refuse and never attempts to be the authority.
  • A refusal is shown as a refusal, not as a failure.

12.3. Safety

  • A destructive action confirms first, and the confirmation says what will be lost. "Are you sure you want to delete account 'jdoe'?" is the shape.
  • Deletion is soft wherever anything references the record, and the interface says which of the two it is doing. A deletion that removes history is a different action from one that does not, and they must not look the same.
  • Reverting creates a new version. It never removes one, and the confirmation says so.

13. Naming and the default column set

For an entity named book in a component named refdata:

Thing Convention Example
Domain type PascalCase singular Book
Screens PascalCase, suffixed BookListPage, BookDetailPage, BookHistoryPage
Query hooks use plus the operation useBooks, useSaveBook, useDeleteBook
Protocol file singular book_protocol.ts
HTTP routes plural, kebab /api/books, /api/books/:id
Subjects component, version, plural, verb refdata.v1.books.list
Wire fields the server's C++ names modified_by, never modifiedBy
Domain fields camelCase modifiedBy
Interface labels Sentence case "Job title"

The interface says "Delete" and "Add", not "Remove" and "Create", except where a record is detached from a parent rather than destroyed, where it says "Remove".

13.1. Hidden by default

version, modified_by, performed_by, recorded_at, change_reason_code, change_commentary, tenant_id, and description on every entity.

id is never shown and never offered, because nobody reads a UUID. It is available through a copy action on the detail screen.

The Qt client hides description on every entity by rule and the audit fields by declaration. Same result, and the rule is worth keeping.

14. Where the Qt client disagrees with itself

Recorded so the port does not inherit a defect by accident. In each case this document takes the generated standard.

The accounts screen The standard, and here
Create and delete never prompt for a reason Every write prompts; see section 7
A pure field edit is sent with empty audit fields Every write carries a reason
Six bespoke tabs One tab per declared field group
Multi-selection with bulk actions Single selection; bulk is a separate feature
A per-entity delegate class One shared renderer driven by column declarations
Sorts on the displayed string Sorts on the underlying value
Declares a default column width nothing applies The declaration is honoured
A bespoke history dialog with a two-field diff The generic compare dialog with a real diff
Disabled rather than hidden for unavailable actions Hidden
Delete disabled in create mode Delete not rendered in create mode
Modal boxes for load and save errors A screen-level message, retryable in place
No inline field validation Inline messages on blur
No search or filter on the list Search and a type filter, on the list

The last row is not a disagreement but a gap. The Qt standard list screen has no search box and no filter. Every other CRUD application has one, and a person looking for one record among four hundred will not page through to find it. Search is part of this specification even though the reference lacks it.

15. Implementing a new entity

Work through this in order. Each step ends in something checkable, so a mistake is caught where it was made rather than three steps later.

15.1. Before writing code

  1. Confirm it is an entity. It has all six properties in section 2. If it does not, say so and agree the exception before proceeding.
  2. Classify it. Reference or transactional. Lookup or free-form. Temporal or not. That decides its columns, its sort order, and whether history exists.
  3. Name the collection and the display field.
  4. Group its fields. Each group becomes a tab. A group with one field is usually a mistake.

15.2. Protocol

  1. Add the request and reply schemas for list, save, delete and history.
  2. Add the subjects.
  3. Prove a round trip against the running service, using a rejected or empty case as well as a good one, because a well-formed rejection proves the wire format where a success might not.

15.3. Endpoint

  1. Add the routes: list, get, create, update, delete, and history where it applies.
  2. Validate every input at the boundary and map every failure to a status.
  3. Assert that no field the entity marks as secret appears in any response.

15.4. Screens

  1. Declare the columns once.
  2. Build the list screen from the shared shell, including search and the type filter.
  3. Build the detail screen with the field-group tabs and Provenance.
  4. Build the history screen where the entity is temporal.
  5. Wire the audit prompt into save and delete, including the diff-driven reason filter.

15.5. Verification

  1. Drive the real screens in a browser: list, search, filter, open, create, edit, delete, and history where it exists.
  2. Assert the states that are easy to forget: empty, filtered-empty, error, loading with existing data, and unauthorized.

15.6. Finish

  1. Add the entity to the navigation.
  2. Record anything about this entity that would surprise the next person.

16. Exceptions

Some entities genuinely are not regular. The exception is named and justified, never discovered later by someone wondering why this one is different.

Kind What differs
Lookup Few fields, may be edited inline, no history
Junction No detail screen of its own; managed from a parent
Child Never listed independently; a section of its parent
Singleton One record by definition, so no list and no add or delete
Read-only Derived or imported, so no save or delete
Transactional Most recent first, and deletion usually prohibited

An exception changes which screens exist and which actions appear. It never changes the layout of the screens that do exist, and it never introduces a new icon for an existing concept.

17. Sources

Derived from the Qt client. The documents worth reading alongside this one:

  • projects/ores.qt/api/docs/entity_controller_pattern.org - the four-layer stack
  • projects/modeling/qt_facet.org - the generated facet and its templates
  • projects/modeling/entity_lifecycle.org - the complete per-entity file inventory across every layer
  • projects/ores.web/modeling/component_specification.org - how a component is declared and where its screens sit
  • projects/ores.web/modeling/icon_reference.org - the icon vocabulary, in this repository
  • doc/knowledge/ui/icon_guidelines.org - the source catalogue, in the C++ checkout
  • projects/ores.qt/api/include/ores.qt/ColumnMetadata.hpp - the column styles
  • projects/ores.qt/api/include/ores.qt/EntityListMdiWindow.hpp - the list window contract
  • projects/ores.qt/api/src/ChangeReasonDialog.cpp - the audit prompt
  • The generated templates under projects/ores.codegen/library/templates/, which are the authoritative implementation of the standard

Emacs 29.3 (Org mode 9.6.15)