System reference · spine

Glossary

The canonical domain vocabulary — generated from source, never hand-edited.

generated from CONTEXT.md 5c76fd3fa1bc222 terms

Naming

SimOps (public brand) vs SimOops (internal codename)
Decided 2026-07-07 (marketing-site grill): the product's public-facing name is SimOps — one "o", matching the sim-ops.io domain — used in all marketing copy, the app UI wordmark, docs prose, and customer-facing documents. SimOops (double "o") is the internal codename only: the repo, package names (@simoops/*), database names, container names. Never mix the two in one audience: customer-facing text always says SimOps; code and infra identifiers stay simoops. _Avoid_: "SimOops" in any public copy; renaming code identifiers to match the brand.

Spatial / map entities

Primitive entity kinds
The three first-class on-map entity primitives in the model today, distinguished by their geometry shape and lifecycle:
Auxiliary pin
A *display-axis* classification, distinct from the storage-axis "auxiliary point primitive" above. An auxiliary pin is an on-map entity whose UX shape is: pointlike geometry, rendered as an icon pin (not text), opened via click→properties-panel (read-only data + per-kind workflow actions), with the edit modal reached via the panel's Edit button or E key. Auxiliary pins are not members of SpatialEntityCollection (no multi-select participation) but do participate in SelectionService via a single selectedPin$ : {kind, data} | null slot, mutually exclusive with the entire spatial axis — both the Multi-selection's primary and its multiSelection map. Selecting a pin clears any worker / plant / area / road selection (single or multi); selecting any spatial entity clears the pin. Modifier-key clicks on a pin are silently treated as plain-click (the pin axis doesn't support add/toggle since pins are solo by construction).
Position provenance
How a Auxiliary pin's point was obtained — recorded only on the field-capturable pins (Observation, Incident and Blocker — all WorkGroup-backed). Three sources: manual (a person placed or dragged the pin on the map), device_gps (the capturing phone's geolocation fix), and photo_exif (GPS tags read from an attached photo). Carried with an optional accuracy radius in metres. The coordinator-placed pins (delivery / alert / poi) have no provenance — they are always, implicitly, manual. _Avoid_: source (overloaded), origin; geotag (the act of tagging, not the recorded property).
Inferred location
A Position provenance that is *not* manual — i.e. device_gps or photo_exif: the point was derived from a sensor rather than deliberately placed, so it is approximate and carries an accuracy radius. It is surfaced to users *as* approximate — a "GPS / photo location, ±N m" affordance on the properties panel and a subtle radius ring on the map — distinguishing a sensed point from a deliberately placed one. Manually correcting an inferred pin (drag or edit) reverts its provenance to manual and clears the radius; the original fix survives only on the pin's position-update timeline event. _Avoid_: estimated, fuzzy, guessed; "GPS pin" (conflates the source with the pin).
Multi-selection
A set of zero-or-more SpatialEntitys the user has explicitly grouped for *bulk operations* (delete, hide, lock, move-to-floor, reassign-contractor). Membership is kind-agnostic by design — worker, plant, area, road can mix freely. Auxiliary pins are deliberately excluded (see "Auxiliary pin" above and ADR 2026-05-21-auxiliary-pin-interaction-model.md D5); their interaction model is click→panel→workflow-actions, not bulk ops. Selecting a pin (any modifier) wipes the multi-selection — the spatial axis and the pin axis are full-mutex in both directions.
Bulk-action strip
The UI affordance that takes over the side panel when the multi-selection contains entities of more than one kind. Lists the bulk operations that apply across the mixed set (delete, hide, lock, move-to-floor, reassign-contractor) and nothing else — no per-entity properties. To edit an individual entity, single-click it out of the strip.
Disambiguation picker
A small popover that appears when a click lands on a stack of overlapping entities (>1 hit at the click point). Lets the user pick which one(s) to act on instead of guessing layer order.
Floor focus
A focused building floor acts as a selection lens: while it is active, every spatial gesture (single-click, box, lasso, Disambiguation picker, right-click, drag) and every attribute-based selection tool (by type, by contractor, by query) acts only on entities on the focused floor. With no floor focused, the site default applies — ground-level entities are selectable, upper floors are not shown. An entity counts as on the focused floor if it shares that floor in the same building *or* sits at the same physical elevation in a neighbouring building. Travelling *to* an off-floor entity happens off the map: selecting it from the Site member roster list, Site browser, or search focuses its floor and flies to it. Those non-spatial paths are deliberately exempt from the lens.
Off-floor hint
An on-map entity rendered dimmed because it sits on a building floor other than the one in Floor focus. It exists purely as a spatial cue — "something is over here on another floor" — and is fully inert: not hoverable, clickable, draggable, selectable, or reachable through the Disambiguation picker. To act on it you navigate to its floor (or reach it from the roster/search). Spans worker, plant, and ActiveArea; Auxiliary pins carry no floor and are never hints. Decided 2026-05-28 (grill session). _Avoid_: ghost (overloaded — the audit overlay renders "ghosts"), faded token (worker-specific; the concept is kind-agnostic), dimmed entity (names the rendering, not the inert-ness).
Noticeboard
The dock panel of live operational items grouped by kind — Alerts, deliveries, the permit-coverage advisory, and Clashes (split "relevant" vs "other" by the viewer's vantage). The operations board — distinct from the Health & Safety panel.
Health & Safety panel
The dock panel hosting the site's H&S records — Incidents, Observations, Inspections and Actions sections — opened with H. Distinct from the Noticeboard. Note the asymmetry: the *Incidents section* is one of four — the panel as a whole is Health & Safety, not "incidents".
Site browser
The dock panel that lists the site's on-map objects grouped by kind — as a *non-spatial* way to find a thing and travel to it — covering both the selectable primitives (Workers, Plant, ActiveAreas, roads, PointOfInterests) and the Reference features building and zone under their own groups (ordered buildings → zones → roads, most- to least-prominent substrate). Together with the Site member roster and search, it is one of the off-map navigation paths deliberately exempt from the floor-focus selection *lens*.
Association snapshot
A one-shot read of the current multiSelection, used to pre-populate a domain modal that associates entities with a parent — incidents (involved_entities) and Smart Groups today. Entities outside the domain's accepted kinds (e.g. road for incidents) are filtered out and surfaced as a "skipped: N" hint, not silently dropped.
Variant
A value on a primitive's variant enum (e.g. PlantType.crane, FeatureType.work_area). Variants influence rendering, clash semantics, and creation-flow rules, but share the primitive's table/geometry shape.
Phase
A cross-cutting classification applied *across* primitives — values are construction | commissioning | handover. Decided 2026-05-19 (grill session): phase is derived from the variant by default, with an explicit nullable override when the variant doesn't determine phase uniquely. The mapping variant → default_phase is the single source of truth; the override exists for genuine independence cases (a WorkerType.construction worker doing handover snagging; a crane reused across phases).
Group
Two existing concepts that the codebase already names "group":
MAP_ENTITY_REGISTRY
A frontend TS const (no backend counterpart) keyed by MapEntityKind, holding per-kind hooks for the *behaviour* axis of on-map entities: drag, lock, hide, position update, floor-aware drop, drag-eligibility under workflow status. Membership is the union of every on-map kind that participates in those behaviours: worker | plant | road | area | poi | delivery | alert | incident | observation | blocker | textlabel. Each entry exposes getSelectedEntity(selection), getLocked(entity), canDrag(entity), toggleLock(entity), toggleHide(entity), updatePosition(entity, lonLat), plus per-kind constants (defaultLocked, floorAware).
Variant metadata
Per-primitive registries hold variant behaviour and identity. The existing FEATURE_TYPE_REGISTRY is the shape to follow — when adding WorkerType and extending PlantType, parallel WORKER_TYPE_REGISTRY and PLANT_TYPE_REGISTRY are added. Each per-primitive *TypeSpec carries the fields meaningful to that primitive (e.g. requires_levels on FeatureTypeSpec, default_radius_m on WorkerTypeSpec).
Interaction mode
What a click on the map does *right now* — the single active modal behaviour, one of: idle (normal selection), creating a primitive (worker / plant / area / road), placing an Auxiliary pin (delivery / poi / alert / incident / text), measuring, positioning a floor plan, or picking an analysis anchor. Exactly one is active at a time; the modes are mutually exclusive by convention, not by an enforcing owner. Decided 2026-05-29 (grill session): the mode is a *derived aggregate* — computed from the several services and component inputs that each own one mode, surfaced as one value under a fixed precedence — not a single owned state. The "in some placement/creation mode" predicate the map's cursor and click-short-circuit logic keys off derives from it. See docs/decisions/2026-05-29-map-event-wiring-host-decomposition.md. _Avoid_: creation mode (only one variant of it), placement mode (overloaded — names both the aggregate and the auxiliary-pin subset).

Clash engine seams

Clash rule identity
Every clash identifies the rule it violated via a non-empty rule_name — the canonical "which rule was breached" label, surfaced in the UI with the hand-raised glyph. This is an invariant, not an optional field: the engine always sets it from the rule's name, and it is NOT NULL wherever it is persisted (ClashStance). Decided 2026-06-02 (grill session): a missing rule_name is treated as impossible, not survivable — there is no UI fallback; it is enforced by a required frontend type and a backend guard against empty names.
Clash category
A per-variant declaration of *how a variant participates in clash rules* — distinct from the variant itself. Current members: physical_worker, live_hazard, mobile_plant. Decided 2026-05-19 (grill session): rules filter on clash category, not on enumerated variant lists. Adding a new variant means declaring its category; existing rules don't churn. The category lives on the variant's spec entry in the per-primitive registry.
Enclosure
The containment context an entity occupies for proximity purposes: the building it is placed inside (its building_feature_id), or the open air when it has none (building_feature_id is null). Two entities share an enclosure when they are in the *same* building, or *both* outdoors; a building wall is the boundary between enclosures. "Inside a building" means the building_feature_id FK is set — an explicit placement-with-a-level — not the mere geometric overlap of a footprint over a building outline.

Clash lifecycle and consequences

Clash episode
The persisted, first-class record of one clash over its lifetime — born when a specific conflict first appears, cleared when it ends. Identified by the same tuple as a clash stance: (site_id, entity pair, rule_name) (see Clash rule identity). Distinct from two neighbours:
Clash event
The edge-triggered DomainEvent a Clash episode emits at a lifecycle transition, consumed by the RulesEngine to fire a Clash consequence. Six kinds, in two groups by what drives them:
Clash consequence
The effect fired when a Clash event matches a rule — a Notification, Task, or email (any Channel the engine supports). Reuses the notification RulesEngine wholesale: a clash consequence is a NotificationRule keyed to its clash rule by FK, authored inline on the clash rule (one surface) but dispatched by the existing engine (one path). A create_task consequence ("you need to resolve this") auto-closes on clash_cleared and is manually closable by a coordinator stance.
Clash policy matrix
The authoring lens over the clash-rule set (ClashRule): for a chosen token-type pair, a permit-code × permit-code grid whose cells each carry a severity and an optional Clash consequence, above a baseline-proximity strip for the unconditional (no-permit) rule. One surface — no separate rule editor. Cells and the baseline compile to ordinary ClashRule rows; the matrix stores nothing new, it is a projection.
Clash suppression
A per-site off switch for a seeded clash cell. Authored on the Clash policy matrix as the cell severity Off (gated to canManagePermitConfig, behind a confirm), it compiles to a ClashRule with suppresses = true that carries the shadowed seed's filters and rides the per-site axis to outrank that seed: the engine *claims* its signature on the pairs it matches but emits no clash, so the cell is dark for that site alone. Because the claim is signature-scoped, turning a baseline cell Off silences only that hazard — a distinct permit hazard on the same pair still surfaces (it has a different signature); a more-specific rule of the *same* predicate likewise still outranks the suppression. A suppression only ever silences a pair that would *actually* clash. Decided #4, 2026-06-30. _Avoid_: "disabling the rule" — a disabled (enabled = false) rule is invisible to both the engine and the matrix, so the seed keeps firing; suppression is the deliberate, visible per-site silence.

Rule version history

Rule version snapshot
The frozen, full JSON state of an authored rule captured on every mutation (create / update / enable / disable / clone / delete) — the unit the editor's history view, diffing, and (clash today) revert read back. Append-only; the version row carries no FK to its rule, so history survives the rule's deletion (the delete-snapshot is written *before* the row goes). Per-rule version numbers are monotonic, allocated under a per-rule advisory lock. Spans all three authored-rule families — the ClashRule, the NotificationRule, and the ThresholdRule — which share this lifecycle (seeded-immutable-except-enable/disable, clone-to-customise) but not their field sets. _Avoid_: revision (reserved for IncidentCommentRevision); audit log / audit trail (the durable AuditLog / GDPR store — a version snapshot is rule-editor history, not the site audit trail).
Rule version log
The shared seam every authored-rule family records through — RuleVersionLog over a RuleVersionAdapter port (backend/app/services/rule_versioning/). The port carries the one genuinely per-kind thing — the snapshot projection — plus the version model and operation enum; revert (rebuild a rule from a snapshot) and change-summary are *opt-in capability adapters* (clash supplies both today; the notification/threshold twins gain them by adding a hook, never by editing the core). The pure dict-diff is shared and per-kind-agnostic. Replaces the three hand-rolled version services. See docs/decisions/2026-06-13-rule-version-log-consolidation.md. _Avoid_: version service (the superseded per-kind hand-rolls); rule history (too vague — names the data, not the seam).
Rule shape
The portable clash-rule field set and its one owning module — backend/app/services/clash/rule_shape.py. FIELDS is introspected from the ClashRule model minus an explicit excluded set (row id + timestamps), so a field added by migration is carried by default; IDENTITY (site_id, profile_id, source — where a rule *lives*, not what it *says*) is explicit for the revert/seeder skip policies. The interface is to_snapshot / from_snapshot only — additive writer, tolerant reader (a key missing from an old Rule version snapshot hydrates to the model column default; stored snapshots are never migrated). Every ClashRule reconstruction site consumes it (the version adapter's projection + revert, the at-time rule reconstruction, the boot seeder); compile() stays in RuleCompiler, which derives site_specific from site_id at compile time. _Avoid_: hand-enumerating ClashRule fields at a call site (the #1688 dropped- field defect class); rule schema (the Pydantic create/update surface, which stays schema-driven and is only guard-tested as a subset of FIELDS).

Surfacing — bringing site activity to users

Noticeboard
Dock panel today (PanelStateService.noticeboardOpen). Shows shared site state — alerts, deliveries, clashes — that is the same for every user with access to the site. No per-user state; if you and I both have the noticeboard open, we see the same rows. Its rows span both temporal-anchoring classes: the delivery and clash lists are Scrubber-anchored surfaces, the alert list is a Now-anchored surface. _Avoid_: notification panel, inbox
Notification
A per-user inbox item representing something directed *at this user*. Has per-user lifecycle state (unread, read, dismissed). Surfaced via the bell icon + count badge in the top toolbar, opened as a dropdown. Distinct from Noticeboard: notifications are personal, the noticeboard is shared. The same underlying domain event (e.g. "new incident filed") may produce both: a Noticeboard row (everyone sees site activity) *and* a Notification per targeted user (the HSE coordinator gets pinged).
Push subscription
A per-device web-push registration belonging to a user — the row that lets SimOops reach a closed browser. User-global (no site axis: a subscription is a fact about a device, not a membership; site scoping stays in rules-engine audience resolution). One user has many (phone PWA, desktop browser). Only the four personally-actionable notification families push (Task minted for you / assigned to you, @mention, outcome of a thing you raised) — never the Noticeboard firehose. Decided 2026-07-18 (wayfinder map #1293); see docs/decisions/2026-07-18-web-push-channel.md. _Avoid_: push token (mobile-native connotation), device registration.
Briefing
A per-user, on-entry roll-up — the surface a user meets when they enter (or refresh) a site: "what do I need to know and act on right now, and where do I go for it". Composed of modular briefing cards, each summarising one underlying surface (active Alerts; work-stopping Clash blockers and *my* clashes, consolidated into one card; *my* open Tasks; the Notification inbox; open incidents — with weather warnings and day-over-day changes as planned future cards, deferred until their data sources exist).
Scrubber-anchored surface
A dashboard surface whose contents are read as of the scrubber's viewDateTime — it moves with time-travel, so cycling the scrubber to a past or future moment shows what was (or will be) true then. Members today: the Noticeboard clash list, the Noticeboard delivery list, and the incident-list panel body. The defining test: the thing has a *position or extent in time* — a planned Active period, a moment of conflict, a report date — so "as of when?" is a meaningful question. Decided 2026-06-04 (grill session). _Avoid_: time-filtered (overloaded — every panel filters by something), historical view (implies past-only; the scrubber runs forward too).
Now-anchored surface
A dashboard surface whose contents are defined by liveness / workflow status, not by a position in time — outstanding work that is "now and ongoing" until someone acts on it. It is immune to the scrubber: cycling to a past or future moment never changes it. Members today: the active-alert list and count, and the open-incident count badge (draft / submitted / in_review, all-time). Synonym: *live backlog*.
NotificationRecipientState
The per-user state row for one user's view of one Notification. Carries per-user lifecycle timestamps. Unique on (notification_id, user_id).
Audience (notification dispatch)
The resolved set of user_ids a notification was fired to. Materialised at fire-time into NotificationRecipientState rows; not backfilled when memberships change later.
Dispatcher (per-kind helper) — historical, V1 only
A Python helper function — one per notification kind — that knows how to compute the audience for *that* event and calls the generic notifications.dispatch(content, recipient_user_ids=...). Lives in backend/app/services/notifications/dispatchers/<kind>.py.
DomainEvent
A typed, structured fact emitted by a domain service after a state change has committed (e.g. IncidentSubmitted, ReportReviewRequested, WeatherThresholdCrossed). Carries a small payload (IDs + key fields), a site_id, and an actor. Pure data — no behaviour, no DB session. Emitted into the EventBus; multiple subscribers may consume.
EventBus
The in-process pub/sub seam that DomainEvents flow through. Has at least three near-term subscribers: RulesEngine, external-webhook delivery, and outbox writer. The existing broadcast_entity_event (WebSocket fan-out for entity CRUD) is deliberately not routed through the bus — it's a different surface (per-connection room broadcast, not domain event).
RulesEngine
The bus subscriber that evaluates per-site NotificationRule rows against incoming DomainEvents and calls notifications.dispatch() for matching rules. Replaces the per-kind dispatcher helpers as those are migrated.
NotificationRule
A per-site row authored by a coordinator/admin that says "when DomainEvent X fires AND payload matches conditions Y, then fire effect Z". Effects today are dispatch_notification (FYI inbox item) and create_task (see Task below). The shape: - trigger: an event kind enum (e.g. weather_threshold_crossed, incident_submitted, report_review_requested). - conditions: a recursive boolean predicate tree (all / any / not + leaf clauses with field, op, val). Mirrors the existing clash-rule predicate composition (see Clash engine seams). - audience: set algebra (union / intersection) over *audience kinds*: role: hse, contractor_members: $payload.contractor_id, payload_user: $payload.reporter_id, specific_users: [...]. Resolved at fire time to a set[UUID]. - effect: dispatch_notification (Notification + per-user state rows) and/or create_task (Task + TaskAssignment rows).
Task
A *pending action* assigned to one or more users, distinct from Notification (which is informational). A Task has shared lifecycle (open / resolved_by:<user> / expired / cancelled) — resolving by any assignee clears it for the rest. Has an action_kind (e.g. approve_incident, review_report, approve_commissioning_placement), a target entity reference, and an optional due_at. Surfaced via the My Tasks panel (né *My Work*, renamed 2026-07-17, #1257); not shown in the bell dropdown (which is notifications-only).
My Tasks
The dock panel listing *my open Tasks* — the single task inbox (dock id tasks, pencil-square icon, T keybind). Formerly My Work, a two-tab container that also carried a Close out tab (the bulk end-of-day Completion record capture, "close out today"). Decided 2026-07-13 (grill session). Amended 2026-07-17; shipped 2026-07-17 (#1257) (close-out re-scope, see docs/decisions/2026-07-17-close-out-two-verbs.md): the *Close out* tab is deleted — bulk completion moves to the site browser's multi-select — and the now single-tab panel un-wraps and renames to My Tasks (prefs v15 migration; a persisted-open my-work occupant migrates to tasks on load). _Avoid_: My Work (the retired container — the panel is *My Tasks* again); task list (close-outs are not Tasks).
Channel
A delivery transport for a Notification or Task. Today's notifications are implicitly single-channel (in-app via WebSocket). The rules engine makes channel an explicit axis: each rule's effect declares which channel(s) to use, and a *channel adapter* (per-channel subscriber, e.g. email_adapter, sms_adapter) handles delivery on the EventBus.
TaskAssignment
The per-user row for one user's view of one Task. Carries acknowledged_at (saw it) and lets the engine cancel/recreate assignments as audiences change. Unique on (task_id, user_id). Distinct from NotificationRecipientState (which carries *read* state, not action state — Task resolution lives on the Task row, not here). _Avoid_: task_user, task_inbox_entry.
Read state
NotificationRecipientState.read_at is the single per-user lifecycle field. NULL means unread; non-NULL means read. The bell badge count is COUNT(*) WHERE user_id = me AND read_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()).
Click-through target
The Notification row carries structured target columns, not a URL: panel_hint (which panel to open), source_entity_type + source_entity_id (which entity to select). A small frontend NotificationClickRouter switches on source_entity_type to call the appropriate panel-open + selection actions.
WebSocket audience: specific_users
A new AudienceType literal added to backend/app/core/websocket_events.py: AudienceType = Literal["owner_or_shared", "non_owner", "specific_users"]. The payload for specific_users is { user_ids: list[UUID] }. The existing _AUDIENCE_HANDLERS filter pipeline gains one entry that matches the connection's user_id against the list. This is the only WebSocket-layer change needed; the notification subsystem composes over it.

Mentions

Mention
A directed reference to a Site member roster user, embedded inline in user-authored prose (e.g. an IncidentComment body). A new Mention emits a DomainEvent consumed by the RulesEngine, firing a Notification of kind mention to the referenced user, pointing back at the surface where the mention was made. Cross-cutting — the same concept is used wherever a Mention-bearing surface accepts free-text input from users (today: incident and observation comments, via the shared discussion thread; near-term: Action and Inspection comment threads, and any narrative fields those grow). A single notification kind (mention) and a single seeded NotificationRule mean adding a new surface is a per-surface plumbing change; the engine layer is reused unchanged.
Mention pool
The set of users eligible to appear when a user begins authoring a Mention. Decided 2026-05-25 (grill session): the entire verified Site member roster, unfiltered by draft visibility, role, or contractor. The mention's job is to *pull a user into a thread*; pre-filtering pre-empts that intent. A mention sent to a user who can't see the underlying entity still fires; on click-through, the existing graceful-empty pattern applies (panel opens, row absent — see Click-through target).
Mention-bearing surface
A persisted text field that accepts Mentions. First landing: IncidentComment.body; observation comments followed via the shared discussion thread. Near-term: Action and Inspection comment threads (and any narrative fields those grow). Adding a new mention-bearing surface plumbs the parser + emit-event into the surface's write path; the notification kind, audience resolver, seeded rule, and user pref row are reused unchanged.

Presence and identity

Site member roster
The set of verified SiteMembership rows for a site — the answer to "who's on this team?". Pending (unverified) members are excluded; they live in the admin/coordinator review surface, not in operational displays. The roster is the single source of truth for per-user identity attributes (display_name, role, contractor_id) consumed across the dashboard. _Avoid_: site users (overloaded with auth/identity), team list.
Presence
The per-session liveness overlay on top of the Site member roster. Tracks which roster members currently hold an active session in the site room (site:{site_id}), whether they're editing an entity, and where their viewport is centred. A user is "online" iff they appear in the presence list; "offline" iff they're in the roster but not in the presence list. Presence carries *status*, never *identity* — name, role, and contractor are read from the roster.
Role badge
The visual indicator of a user's SiteRole. Appears as a small corner overlay on presence bobbles (online and offline alike) and as a labelled chip in the Presence dropdown rows. Combines an icon (from heroicons) with a role-tinted background — six roles, six (icon, tint) pairs, extensible via a ROLE_REGISTRY frontend constant (TS mirror of the same registry shape used by MAP_ENTITY_REGISTRY and AUXILIARY_PIN_REGISTRY). Roles not present in the registry (e.g. a new backend enum value not yet known to the frontend) render unbadged — no guessed fallback — and sort to the bottom of their section. The current user's own toolbar bobble is deliberately unbadged; the dropdown row representing them carries the full chip. _Avoid_: role icon, role tag, role chip (each describes a sub-part, not the atom).
Presence dropdown
The hover popover anchored beneath the toolbar presence bobbles. Two sections — Online (active sessions) and Offline (roster members not currently connected) — each sorted by role priority (admin → coordinator → hse → member → viewer → client) then alphabetically by display name. Includes the current user with a "(you)" annotation, in the Online section. Scrollable when content overflows a capped height; section headers stay sticky during scroll so the bucket label is always visible. Offline rows render at reduced opacity but keep contractor colour, role badge, and name intact — identity stays readable, only liveness recedes. Empty section headers are hidden. Non-interactive in V1 (no per-row click behaviour); the on-map viewport-cursor feature remains the affordance for "where is X looking right now?".

Incident discussion

IncidentComment
A single user-authored post on an incident's discussion thread. Carries a body, an author, timestamps, and an optional set of IncidentCommentAttachments. Allowed in every workflow status (draft / submitted / in_review / approved / rejected) and frozen only when WorkGroup.status = archived. Body is editable by the author at any time (each edit writes a IncidentCommentRevision row); whole-comment delete is allowed by the author (soft-tombstone) or by holders of incident_comment_moderate (hse + admin) for moderation, with a recorded reason. The body is a Mention-bearing surface — @-tokens in the body fire Mention notifications on POST/PATCH per the diff-based emission rule. _Avoid_: note, reply, message, post, remark.
IncidentCommentAttachment
A file attached to an IncidentComment. Stored in a table separate from IncidentEvidence for forensic separation — the formal H&S record (initial evidence locked at submission) must remain distinguishable at the data layer from discussion artefacts uploaded later. Same 25 MB cap and same upload helper as IncidentEvidence; soft-deleted via removed_at rather than hard-deleted, with a faded placeholder visible in the UI. _Avoid_: comment file, discussion attachment, secondary evidence.
IncidentCommentRevision
An immutable snapshot of an IncidentComment's body taken when the author edits it. The revision row stores the *pre-edit* body plus replaced_at + replaced_by_user_id. Drives the "(edited)" indicator + clickable diff in the comment UI. Comments are never hard-deleted; revisions are never edited. _Avoid_: version, history entry, edit record.
Relationships
- An IncidentDetail has zero or more IncidentComments. - An IncidentComment has zero or more IncidentCommentAttachments and zero or more IncidentCommentRevisions. - A new IncidentComment writes an IncidentEvent (event_type=commented, the previously-placeholder enum value becomes load-bearing); a delete writes IncidentEvent(event_type=comment_deleted). Self-edits do *not* write timeline events — the revision row carries that history.
Distinguishing from sibling concepts
- vs IncidentEvent (backend/app/models/_incidents.py): events are *immutable workflow markers* on a per-incident audit log (submitted, approved, …). Comments are mutable discussion content. The two *intersect* — a posted comment writes an event for timeline coherence — but they answer different questions. - vs IncidentEvidence (same module): evidence is the *formal H&S record* uploaded at the time of incident creation, locked when the incident is submitted. Comment attachments are *discussion artefacts*, separable by table at the audit layer. - vs Notification: a notification is a per-user inbox item that *may be triggered by* a new comment (the dispatcher pings thread-participants + reporter + all hse/admin). The comment is the content; the notification is the surfacing.
Permissions
Two new Permission enum members: - incident_comment — granted to member / coordinator / hse / admin. The reporter can comment on their own incident regardless of role (matches the existing reporter-bypass pattern for can_edit / can_submit / can_delete in compute_incident_permissions). - incident_comment_moderate — granted to hse + admin only. Hard-deletes any comment (soft-tombstone with reason); writes IncidentEvent(comment_deleted).

Incident investigation

Investigation risk axes
Nullable likelihood × consequence (1–5 each) on an incident, rating recurrence risk to size the investigation — deliberately the same 5×5 arithmetic and derived-band vocabulary as Observation risk rating, not a second matrix grammar (Frida's 3×3 was rejected). Distinct from the incident's flat severity, which stays the *outcome* classification. _Avoid_: severity (the outcome axis); risk matrix unqualified (say which instrument).
Investigation level
The band (low/medium/high/critical) derived from the Investigation risk axes — how deep the investigation goes, not how bad the outcome was. _Avoid_: severity band (the observation/incident outcome vocabulary).
Incident cause
A typed causal factor on an incident: free-text cause + a type from a per-site seeded registry (the Observation category pattern), optionally pinned to a Barrier layer and/or a Timeline step. Repeatable — supersedes the single free-text root_cause, which freezes read-only. _Avoid_: root cause (the frozen legacy field).
Barrier layer
The Swiss-Cheese defence a cause breached — organisation | supervision | conditions | unsafe_acts | technical_failure. A column on Incident cause; the column is the app's Swiss-Cheese model. _Avoid_: layer unqualified; slice.
Timeline step
An ordered Chain-of-Events step (label + narrative) on an incident investigation; causes pin to steps; the terminal step is the incident itself by convention. V1 edits as an ordered list — the drawn-timeline canvas is a later rendering upgrade, not a schema change. _Avoid_: event (an IncidentEvent is the audit-timeline row — a different thing); step of the workflow (this is causal narrative, not FSM).
Investigator
The nullable assigned-investigator slot on an incident, set by HSE (typically at review start). "Classifier" remains a permission (classification amendment), never a slot. _Avoid_: reviewer (the band, not the assignment).
Incident closure (derived)
An incident is *closed* when its workflow is terminal (approved/rejected) and every child Action is terminal (accepted/cancelled) — computed at read time, never stored. Frida's "Implementation" phase = approved with open actions, which falls out for free. _Avoid_: case-closed flag (a stored bit was rejected — it can contradict the derived truth).

Observation

Observation
A Safety Observation Report: an on-site safety sighting — an unsafe act, unsafe condition, or safe behaviour (ObservationType) — raised by anyone on site, including anonymously (in-app or via a QR Observation capture link), and gated through HSE Observation triage before it reaches the wider site. Modelled as a WorkGroup(group_type= observation) with a 1:1 ObservationDetail extension and surfaced as the fifth Auxiliary pin kind — the heroicons eye glyph, tinted by the derived Observation risk rating band once published (pending pins render dimmed on the HSE-only view). It shares Incident's *shape* — a positioned pin with Position provenance, involved entities, free-text persons, evidence, a Mention-bearing discussion thread, an event timeline — but its own lifecycle: submitted → triaged → published-or-discarded, with map visibility derived from status + band vs the site's Observation visibility threshold (accepted safe_behaviour rows are threshold-exempt), never encoded in the status. Submission emails site hse users immediately; crossing into visibility at triage emails coordinators. Accepted observations feed the report exactly once via the Report observation queue (a section plus SOR summary stats); an open published Observation is a Now-anchored surface item — never a Noticeboard row. Decided 2026-06-11 (grill session), superseding the 2026-06-09 instant-publish identity: see docs/decisions/2026-06-11-observation-triage-rework.md. _Avoid_: incident (the *something happened* record; Observation is the *something was seen* record); the v1 sense "calm instant-publish operational flag" (superseded); inspection / action (sibling surfaces — and note the boundary: an inspection Item result is deliberately never promoted to an Observation); note, sighting, finding.
ObservationStatus
The Observation lifecycle enum: pending_triage (the default at creation) → open (HSE accepted) or discarded (HSE judged not actionable; restorable), with open ⇄ resolved reopenable as before. Deliberately does not encode visibility — who sees an observation is derived from status plus Observation risk rating band vs the Observation visibility threshold, so re-rating never churns status. Distinct from soft-deletion, which remains a separate reversible act reserved for spam/test data, not a triage verdict. _Avoid_: published / logged as status values (visibility is derived); state (overloaded); incident status (a different enum).
Observation triage
The HSE act (hse/admin only) of reviewing a pending_triage Observation: accept or discard, re-rate the Observation risk rating, set or correct the ObservationType and Observation category, and reposition inaccurate pins (routine for no-GPS anonymous submissions, which arrive on an Inferred location). Stamped triaged_by/triaged_at; performed from the triage queue in the Health & Safety panel. Coordinators never triage — the gate exists so HSEs sift observations before coordinators see them. _Avoid_: review / approve / reject (Incident's workflow vocabulary); moderation (the comment-thread term).
Observation risk rating
The 5×5 risk matrix carried by unsafe observations: likelihood (1–5) × consequence (1–5) → score (1–25) → a derived band low/medium/high/critical (fixed boundaries 1–4 / 5–9 / 10–15 / 16–25). The submitter rates at creation via a colour-zoned tap-grid (required for unsafe types; absent for safe_behaviour); HSE re-rates at triage and the band follows; the initial submitter rating is preserved for calibration. The band — not the raw score — is what pins, filters, queue sort, and the Observation visibility threshold speak. _Avoid_: severity unqualified (Incident's flat enum — same band names, a different instrument); priority, urgency.
Observation visibility threshold
The per-site band (Site.observation_severity_threshold, default medium, tunable by hse/admin) gating coordinator/member/viewer map visibility of rated (unsafe) observations: visible ⇔ status ∈ {open, resolved} and band ≥ threshold. safe_behaviour rows carry no band and are threshold-exempt — visible to every role once accepted (decided 2026-07-13, grill session; before that their NULL score never qualified, so positives were HSE-only). hse/admin see everything including the pending queue; a signed-in reporter always sees their own. It is also the default for the Report observation threshold, but no longer binds it — the one knob that served both surfaces was split when the report needed to reach below the map (decided 2026-06-12, see docs/decisions/2026-06-12-observation-report-threshold-split.md). _Avoid_: severity threshold unqualified; "report inclusion" (that is now the separate Report observation threshold).
Report observation threshold
The per-report band floor governing which accepted observations appear in the SOR report section — a separate knob from the Observation visibility threshold, persisted in the report section's form_data, defaulting to the site threshold (so a fresh report mirrors the coordinator map). Lowerable below the site threshold by anyone who can build the report (coordinator and above): below-threshold accepted observations are noise-filtered, not confidential, so an HSE-reviewed SOR may deliberately be more complete than the live map. It moves the band floor only — pending_triage/discarded rows never reach the report at any floor. safe_behaviour rows carry no band and are governed instead by a per-report *include safe behaviours* toggle (stats-only otherwise). Manual per-row check/uncheck (force_included/force_excluded) survives floor changes — the floor is a default line, not a reset. _Avoid_: visibility threshold (that one gates the map); a report-mode "severity filter".
Report observation queue
The exactly-once feed of accepted observations into reports. An Observation joins the queue when Observation triage accepts it and is consumed by the first exported report whose observations section offered it — printed, force-excluded, or left unchecked below the Report observation threshold floor alike: every offered row was surfaced to the report builder, so an exclusion is a recorded decision, not a crack. Draft reports never consume (abandoning a wizard returns nothing to limbo); each consumed observation records which report carried it. This replaced the observed-on-the-report-day window, which could never match — reports target tomorrow while sightings live in the past, so pre-queue observations never appeared in any report once triage lagged the sighting by a day (decided 2026-07-13, grill session). _Avoid_: report window (the temporal entity window, a different axis); backlog (the pending-triage pile — that is upstream of this queue).
ObservationType
Repurposed 2026-06-11 to the SOR three-way: exactly unsafe_act / unsafe_condition / safe_behaviour, now required at creation (three big buttons on every form, including the anonymous one). The v1 hazard-domain values (housekeeping, obstruction, …) moved to Observation category. safe_behaviour — the positive observation in customer speech and report labels — carries no risk rating; once accepted at triage it is visible to every role (a green eye pin, threshold-exempt — amended 2026-07-13, it previously never reached the coordinator map) and feeds the report's positive-observation ratio. _Avoid_: category (that is the hazard domain, a separate field); the v1 value set; kind, nature.
Observation category
The hazard domain of an Observation — a per-site, DB-seeded registry, not an enum (seeded: working_at_height, ppe, scaffolding, electrical, manual_handling, housekeeping, plant_and_equipment, environmental, security, access_egress, other; client-tunable without a migration). Single-valued and nullable at submission — anonymous reporters may not know the right bucket — and set or corrected at Observation triage. _Avoid_: bare "category" unqualified (collides with Clash category); tag, label.
Anonymous observation
An Observation whose reporter is truly unrecorded — reporter_user_id NULL and the audit identity suppressed — whether it arrived via the QR form or a signed-in user choosing *submit anonymously*. No sealed/hidden identity column exists, by design: the anonymity promise is real. Accepted consequences: no see-own/edit-own powers and no notification backflow for the reporter. A non-identifying submitted_via (app / app_anonymous / qr) is kept for triage context, plus an optional volunteered contact field. _Avoid_: pseudonymous (implies a recorded identity); unattributed.
Site boundary
A nullable site-level polygon (authored with the map drawing tools by the coordinator/hse/admin band) marking the operational perimeter — config, not map content: deliberately outside the Reference feature pipeline so import/versioning/bulk-delete never touch it. When unset, the active site map's bounds serve as an explicit degraded fallback. First consumer is Geofenced capture entry; the concept is generic — any future "is this on site?" question (Blocker whole-site scope, stock reasoning) may reuse it. Decided 2026-07-06 (grill session). _Avoid_: geofence (the *check* against the boundary, not the polygon); perimeter, site extent (the raster Site-map capture bounds — a scan footprint, not this).
Geofenced capture entry
The tokenless alternative to the QR poster: one global /observe URL where the server resolves the reporter's GPS fix against Site boundarys (buffered by fix accuracy) and serves only the matched site's capture form — one match goes straight in, several show a picker of matched names only, zero hands off to the poster path. Near-miss site names are never returned: non-enumerability is preserved by physical presence rather than token possession. The fence gates resolution only — a served form's submission is never rejected on location; the server stores containment facts (in-boundary, distance, accuracy) as provenance with submitted_via='geofence', and spoofability is accepted with Observation triage as the firewall. Successful resolution mints a short-lived session token required by the write endpoints (an ephemeral Observation capture link analogue). Decided 2026-07-06 (grill session). See docs/decisions/2026-07-06-geofenced-anonymous-observation-entry.md. _Avoid_: geofenced observation (the observation is ordinary; the *entry* is geofenced); GPS gate (resolution-only, never a submit gate).

Corrective actions — the close-the-loop hub

Action
A corrective work item raised when something on site needs fixing, owned by one responsible company (a contractor, never a user, at creation) and tracked through a review-gated lifecycle to HSE sign-off. A standalone entity — not a WorkGroup extension, not a Task — carrying a description, due date, per-site serial number, optional Action priority, and an optional Parent reference. Members may only route within their own ∪ supervised companies; coordinator/hse/site_manager/admin route to any (the sketch's COMPANY SCOPED vs ALL). Map presence is a badge on the parent's pin, not an own pin. _Avoid_: task (the per-user ping that *points at* an Action); corrective action triple (the frozen ObservationDetail seed fields); action item, todo.
Action lifecycle
assigned → pending_review → accepted | cancelled. Creation always names a responsible company, so a new Action lands in assigned; Action close-out moves it to pending_review; HSE accept terminates; reject is an audited *transition* back to assigned (reason required, count kept) — the rework loop is not a state. cancelled is the terminal escape (an extension beyond the source sketch). Overdue is derived from the due date, never a state. _Avoid_: open/resolved (Task vocabulary); draft (no draft state exists).
Action close-out
The responsible company's evidence-backed claim of completion: any member of the responsible contractor (or HSE/admin on-behalf) submits a solution narrative plus photo evidence — hard-required; stamped with date-completed and who closed out. Enters Action review. _Avoid_: resolve (Task verb); complete (Inspection vocabulary).
Action review
The HSE sign-off gate on a closed-out Action: accept (terminal) or reject back to assigned with a required reason. The review band is hse + admin + site_manager, mirroring the incident review split. _Avoid_: triage (Observation vocabulary); approve (the incident gate's verb).
Action priority
Optional low/medium/high, chosen by HSE — required when the Action is spawned by a failed inspection item — and the input to due-date defaults. _Avoid_: severity (an outcome scale); band (the observation instrument).
Parent reference
The optional polymorphic link from an Action to the finding it remediates — an Observation, an incident, or an inspection Item result. Optional by design (ad-hoc actions are legal) and one-to-many (a parent may carry many Actions; the observation triage prompt raises one by default, prefilled and skippable). _Avoid_: target entity (Task vocabulary).

Inspections

Inspection
A templated safety audit run by an HSE inspector against an Auditee company: checklist items answered with comments and photos, a validation-gated submit ("submit available iff valid"), and one corrective Action offered per failed item at completion. Two-state lifecycle: in_progress → completed — submit is terminal; the inspector *is* the authority, so there is no separate approval step. Completed inspections are read-only, site-visible, scored, and PDF-exportable. _Avoid_: audit (colloquial); survey; observation (the sibling ad-hoc surface — inspection items never become Observations).
Inspection template
The admin-authored checklist an inspection runs against: named, site-scoped, and versioned — editing mints a new immutable version (sections + items + pass threshold) and a run references the exact version it used, so history never mutates. Per item: question, section, comment required/optional, evidence-required (⇒ photo). _Avoid_: inspection type (Frida's column name for the same idea); form; checklist unqualified.
Item result
One answered checklist item on an inspection run: a three-way answer — pass ✓ / fail ✗ / N-A — plus comment and photo per the template item's flags. Deliberately inspection-internal: never promoted to Observation rows. A fail feeds the submit-time action confirmation step. _Avoid_: finding (unqualified); observation (the sibling entity).
Pass score
The completion-time score — passes over non-N/A items, judged against the template version's pass threshold — denormalised onto the completed inspection. _Avoid_: risk rating (the observation instrument).
Auditee
The company an inspection audits: a required reference to the site's contractor registry (a subcontractor or the principal contractor), never free text. Drives the responsible-company prefill of fail-spawned Actions and the completion-notification audience. _Avoid_: contractor name (the Frida free-text field this replaces).

Work tracking — lines, dependencies, snags, lost time

Work line
A first-class, contractor-owned line of programme work — the anchor for Work dependency edges *and* for close-out. P6-mappable core (opaque nullable activity code, description, place band, planned window) so a programme import is a mapping exercise — that import now exists: lines may be born by Promotion from a staged Programme activity, carrying natural-key provenance (source_programme_id + activity code) that keeps them in Sync review|living sync; hand-authored lines are unchanged (all-NULL provenance, invisible to sync). Status not_started / in_progress / complete, moved by explicit start/close verbs; one line = one contractor. Hand-authored (or promoted) by the coordinator band — permit import mints nothing. Tokens attach via a nullable FK: the contractor picks at placement, group-inherited, with a coordinator backstop. Closing a line is the load-bearing "can the next trade start?" verb — evidence-backed when the line is a dependency predecessor. Daily Completion records and the Planner's permit atom are unchanged. See docs/decisions/2026-07-17-work-line-first-class.md. _Avoid_: activity (the planner-sense reality atom and P6's own noun — a work line may *carry* an activity code, it isn't one); task (the notification-pipeline entity); work package (WBS vocabulary).
Work dependency
A planner-authored or programme-imported edge WorkLine → WorkLine — never tokens (tokens churn; the line endures). Carries a link type (FS|SS|FF|SF, mirroring P6; hand-authored edges default FS) and a signed lag (stored in hours verbatim, evaluated as round(/8) calendar days) — the #1205 generalisation, pulled by the programme-import epic (2026-07-21; FS-only was the original shipped shape). Same-contractor edges permitted; self-edges, duplicates (same pair and link type), cross-site edges and cycles rejected (DFS at insert); transitive redundancy tolerated. Edges are immutable (edit = remove + re-add); deleting a line confirm-and-cascades; linking an already-complete predecessor is legal (Born satisfied). The contract per link type (FS: "B must not start until A is complete") yields three derived, never-stored conditions: *violated*, *unverified start*, *satisfied* (kept as record; completion notifies the successor's contractor). Authoring rides the one shared work-line permission (coordinator band; contractors never); imported edges carry provenance columns and are maintained by Apply|sync apply, hand-authored ones are sync-invisible. See docs/decisions/2026-07-17-work-dependencies-and-violation-surfacing.md (+ the 2026-07-21 amendment in 2026-07-21-programme-revision-sync-and-edge-import.md). _Avoid_: token dependency (rejected shape); relationship (the staging row ProgrammeRelationship — an imported edge is its promoted consequence).
Dependency violation
The derived contradiction of a Work dependency — not a clash (clash stays geometric/emergent; a violation is *declared logic contradicted*). A dedicated graph-driven evaluator (on-write + sweep) persists violation episodes (own table; the edge stays stateless) that surface as a third noticeboard item kind above red — never ignorable, never suppressible; the only remedy is fixing the graph or the work. Two kinds, one episode shape: *violated* (predecessor-token ∩ successor-token schedule overlap while the predecessor line is unclosed — time-only, no geometry) and *unverified start* (B's window arrives, A unclosed, no live/future A-tokens — assume the worst). Both carry a one-click, pre-filled declare-Blocker handoff: the violation surfaces automatically, a human declares the blocker. The close-out duty ladder escalates FYI (edge authored) → nudge (last predecessor token ends unclosed) → violation (B arrives). Presentation: user copy says "delays" — "delay" replaces "violation" in *all* user copy (a *delay* is the planning concept: a declared dependency contradicted; internal/ schema/event names keep violation), and delays *create* Blockers — the blocker stays the sole timed-stoppage record, so a "Delays"-named stoppage surface is a Blockers surface. The pin is the hazard diamond; the noticeboard renders an uncollapsible emergency banner; map paint is place-centric (place-band glow, token-less). RuleSeverity is untouched. See docs/decisions/2026-07-17-work-dependencies-and-violation-surfacing.md. _Avoid_: clash (different machinery and severity vocabulary); mega-red (a prototype nickname, not shipped copy); blocker (a violation *suggests* one; only a human declares it).
Snag
A contractor's own-work defect caveat — work not constructed to spec, flagged by the team that built it while recording work completion (the "wee box" riding every record-work-completion gesture, daily or closing; there is no standalone raise button). A snag can never be raised against another trade: a cross-trade grievance is a Blocker. Substrate: the seventh WorkGroup sibling (SnagDetail), anchored by a polymorphic token ref + work_line_id snapshotted at raise. Two stored states, open → closed; the *"complete · N snags open"* caveat (Tony's "98%, not 100%") is derived, never stored — the roll-up to 100% is the join emptying. Close is a per-snag human tick — via a Resolving token's wee box or a direct tokenless close — and both paths sit behind a hard photo gate (422 snag_closeout_photo_required; the completion photo is dual-purposed). Close self-certifies: no verification state — a deliberate departure from industry two-step close (verification migrated to blockers). Soft-delete is the mistake path; no reopen edge — a resurfaced defect is a fresh snag, fresh clock. Snag clocks (raised_at → closed_at) are responsiveness metrics and never feed lost time. Visibility: owning team + principal side (HSE deliberately out — snags sit on the commercial axis, not the safety axis). See docs/decisions/2026-07-17-snag-own-work-caveat.md. _Avoid_: defect / punch item / NCR (industry imports with heavier workflow); snag list / snag board (there is no standalone board — snags live inside the Completions view); blocker (obstruction, not defect).
Resolving token
A Token placed to fix open Snags. The *snag* holds a re-pointable polymorphic ref to it — so "fix scheduled" is derived from the live ref, never stored, and one corrective visit can fix many snags. It inherits the original work line by default; its record-work-completion wee box carries per-snag "fixed?" ticks (a shared token closing 3 of 4 is legal), and closing rolls the completion caveat away (98 → 100%). _Avoid_: rework token, corrective action (Action is the H&S close-the-loop entity — unrelated machinery).
Lost time
The unified, read-only rollup the commercial side reads: crew-hours = actuals window × stood-down headcount, derived, never stored — and the totals count blockers only (Colin: "lost time is the sum of those little blockers"). Snag rows appear as clocks-only responsiveness metrics with zero crew-hours. Capture is one nullable stood_down_headcount on the blocker detail (defaults from affected worker refs, editable; multi-crew = multiple blockers); the blocker's actuals window is the loss window (still-stopped = work_resumed_at NULL or > now). Attribution: *loser* = the blocker's contractor; *responsible* = triage-accepted blame only, stored at triage confirm (pre-derived from the cause chain; ratified 2026-07-17). Served by read-only GET /api/lost-time behind lost_time_view; reviewed in an Analysis-suite tab (dashboard + ledger) with linked-brushing map coupling — choropleth off by default, a "review range" pill that never pokes the scrubber. See docs/decisions/2026-07-17-lost-time-accounting.md. _Avoid_: delay / disruption cost (claims-analysis vocabulary); downtime; lost-time injury (an H&S metric — unrelated); snag hours (zero by design).
Close-out (work sense)
An umbrella naming three distinct gestures, disambiguated by object: work-line close (the load-bearing "can the next trade start?" verb on a Work line), daily completion (a Completion record — optional and silent), and finished-early (an explicit whole-token early-done signal that removes remaining scheduled occurrences behind a confirm-N dialog — the *nuke*, never a ghost). Token expiry ≠ completion: a lapsed window proves nothing about the work. Distinct from Permit close-out (a permit lifecycle transition) and Action close-out (H&S). See docs/decisions/2026-07-17-close-out-two-verbs.md. _Avoid_: close out (bare, without naming which gesture); completion (only the daily record); expiry (a schedule fact, not a statement about work).
Work candidates
The shared suggestion seam every token-tagging mobile flow picks from: the actor's own or oversight-inclusive day-active work (POST /api/sites/{site_id}/work-candidates, scope=own|oversight), derived server-side from membership. "Mine" is eligibility, "near me" is ranking: an optional device fix (POST body, never a URL) only *orders* the list and computes the near_you fact — proximity never adds or removes a candidate, so denying location loses ranking, never function. Adopters: blocker raise, record-completed-work (snag wee-box inherits). Distinct from affected-suggestions (cause-anchored, cross-contractor, coordinator- facing). Decided 2026-07-18 (wayfinder map #1293); see docs/decisions/2026-07-18-transient-geolocation-privacy.md and docs/plans/2026-07-18-geolocation-work-candidates-spec.md. _Avoid_: nearby work (the ranking, not the universe); suggestions (bare — collides with affected-suggestions).

Programme import — staging, promotion, living sync

Programme
The durable, site-scoped series identity of a master P6 schedule — a container of Programme revisions, never itself a file. Carries no contractor (the master spans trades); carries the remembered Band mapping and the two diff-basis pointers (current_revision_id and the Logic pointer). Sites have one in practice (master programme only — per-contractor programmes have no v1 customer). _Avoid_: plan (the removed planning-cycle vocabulary); project (P6's own noun for the file contents).
Programme revision
One immutable handed-over programme file (XER or PDF print), staged as WBS nodes + activities + relationships that are never mutated after ingest. revision_kind (baseline / progress_update / ce_impact) is metadata — CE fragnets are revisions of the *same* series. The raw file lives in object storage (ultimate provenance). source_format is the single fidelity switch: a pdf revision is *degraded* — lines + dates only, day-precision, and Edge-blind. _Avoid_: version (feature-versioning vocabulary); import (the act, not the artifact).
Staging
The role the Programme* tables play: imported truth held at arm's length, browsable but inert. Staged rows never touch the curated graph uninvited — the "only critical chains get lines" philosophy survives the ~450-row firehose. Staging is a role, not a noun: the rows remain what they are after promotion links to them.
Promotion
The coordinator act (gated work_plan.manage) that mints Work lines from a curated selection of staged activities — 1 activity → 1 line, on a chosen ready revision (default latest). Pre-filled from the Band mapping; milestones become zero-duration lines; LOE/WBS-summary rows are hard-excluded; status maps TK_* verbatim (including complete — history has no evidence gate). Auto-materialises staged logic between promoted lines as edges, behind a mandatory preview ("N edges — M violated") and a per-act notification digest. The inverse verb is detach (nulls provenance, keeps the code, event-logged). _Avoid_: import (upload/ingest — promotion is the later, curated act); approve (permit vocabulary).
Band mapping
The per-Programme memory WBS band path → default contractor + place/level, filled once at first promotion (the wizard ceremony), overridable per activity, reused by sync for new arrivals. No fuzzy auto-matching ever writes — suggestion is at most UX garnish.
Sync review
The facet-grouped reading of what a newly-ready revision changes versus the current diff basis: dates / names / logic / orphans / new codes / status disagreements. Delta semantics, not reconciliation — staged rows unchanged between revisions are never touched, so SimOps-side drift is never re-litigated. Fully attended in v1: upload alone changes nothing.
Apply
The single coordinator act committing a Sync review: planned windows (blended dates) and names track the revision; placement, contractor, tokens, evidence preserve; status is surface-only both directions (advance goes through the close verb). Moves current_revision_id (and the Logic pointer iff XER), bumps promoted_from_revision_id (a freshness marker, not a birth certificate), writes per-change events. Never mints lines — new codes are a signpost into Promotion. Edge replay: added → materialise, removed → remove, re-lag → remove+re-add, unchanged → untouched (the free resurrection guard for hand-deleted edges).
Orphaned line
A promoted line whose (programme, activity_code) no longer resolves in the current revision — derived, never a column; reappearance self-heals. Verbs are existing ones: keep (default), detach, close/delete — never auto-anything. Orphans keep last-applied dates; the badge explains the staleness (softened copy when the source revision is a PDF).
Re-key
The coordinator-band identity assertion for the renumber contingency: editing a promoted line's activity_code to the new code ("this line *is* that activity"). Unique-index guarded, event-logged; the whole merge-key fallback — no heuristic matching anywhere.
Logic pointer
Programme.current_logic_revision_id — the edge-facet diff basis, stamped only by XER applies. Keeps PDF applies edge-no-ops and makes an XER-after-PDF apply diff last-XER-vs-new-XER, never "everything added".
Edge-blind
The epistemic stance toward a PDF revision's missing logic: no information about edges, never evidence of absence. A PDF apply touches no edges; a PDF-sourced disappearance badge carries softened copy. _Avoid_: edge-empty (implies a true zero).
Born satisfied
An imported or hand-authored edge whose predecessor line is already complete at creation — legal, inert, the truth of history. Drains most of a mid-project import's violation storm.
Slip chip
The variance atom: a signed-days RAG chip (+14d) comparing a promoted line's planned window against site reality — derived at read time, never stored; thresholds are a per-site setting (default amber +1..+5d, red >+5d). Renders in the board variance lens (toggleable; lens off = board untouched) and the weekly programme health brief. Milestone rows carry a bounded worst-ancestor slip forecast — deliberately not CPM. See docs/decisions/2026-07-21-plan-vs-reality-variance.md.

Blockers — declared work stoppages

Blocker
A declared, first-class record that a cause stopped or impaired specific work over an actual time window. Substrate: a WorkGroup (group_type=blocker) + 1:1 detail extension — the same minting pattern as Incident and Observation — inheriting comments, contractor tagging, Position provenance and the active → resolved → archived lifecycle. It is the sixth Auxiliary pin kind; geometry is nullable — when the cause is itself a pin the blocker renders as a badge on the cause, and only pinless causes ("council closed the access road") get their own pin.
Blocker acknowledgment
An optional coordinator counter-signal on a Blocker — acknowledged or disputed, with a note — that never gates the record's existence. A contractor declaring their own work blocked is self-serving evidence; the contemporaneous acknowledgment (either direction) is the second opinion claims tooling weighs. Decided 2026-07-06 (grill session). _Avoid_: approval, verification (both imply a gate; there is none).

Inventory — materials and stock

Delivery line item
One manifest row on a delivery: free-text description, quantity, unit (plain string; per-site suggestions; no unit conversion, ever — units merge only on equality), and an *optional* Material reference. The manifest is what a delivery *says* it carries; what actually arrived is established by Receive. _Avoid_: cargo, payload; manifest (the collective, not the row).
Material
A thin, site-scoped catalog entry ("rebar 12mm") that line items may reference — the Permit type shape: seeded vocabulary, adapted per site, never forced at entry time. Cross-delivery aggregation ("how much 12mm rebar is on site?") exists only where the reference is set. _Avoid_: product, SKU (implies an org-level catalog; this is per-site).
Stock lot
The received instance of a Delivery line item — the thing that is physically *somewhere*. Carries a polymorphic current-location attribute (laydown ActiveArea FK, point with Position provenance, building + level, or unassigned) as a cache; every location change is a transfer Stock event, so history lives in the ledger. Stock is not time-windowed: when a laydown area's window elapses with stock still in it, nothing auto-moves — an orphaned-stock advisory surfaces it (the elapsed-but-unresolved house pattern). _Avoid_: batch (process-industry connotation); inventory item (the ledger tracks lots, not catalog rows).
Stock event
One signed-delta entry in the append-only stock ledger: delivery_in / consumption / loss / transfer / adjustment, each referencing its cause (delivery, incident, token/permit/area, or a note). Contractor + quantity are mandatory; finer attribution is optional polymorphic refs. Balances are always derived from events; corrections are compensating entries. An incident-linked loss posts immediately — never gated on the incident approval workflow. _Avoid_: stock movement (transfers are one kind, not the genus); stocktake (an adjustment's occasion, not a ledger concept).
Receive
The explicit workflow action on a delivery pin that turns manifest into stock: pre-filled from the line items, edited to actual received quantities (short loads, damage, part-rejection), posting delivery_in events. Status changes alone never post stock — completion is not receipt. _Avoid_: check-in, goods-in (dockyard jargon); complete (a delivery status, deliberately distinct).

Document intake

Ingest
Channel-side arrival of a document — the Mailgun webhook ingesting a forwarded email (IngestAddress, EmailIngest), a file landing via an upload route. Says nothing about what happens to the document next. _Avoid_: using it for the whole pattern (that's Document intake).
Document intake
The pattern itself: a document arrives (Ingest), its genre is declared by the channel's affordance (a button, an address prefix — never a classifier), Extraction optionally drafts structured content, and a human's confirmation IS the domain write — nothing provisional touches domain tables. Degrades to manual entry when no provider is configured. A vertical riding the pattern is a document family (manifest documents, order documents); each family brings its own sibling <X>Document + draft workspace — families share idioms, never tables. _Avoid_: document ingestion (collides with the email-channel term); OCR pipeline (extraction is one optional stage, not the pattern).
Extraction
The LLM step only (services/extraction/): a provider adapter (ExtractionProvider, Claude API / OpenRouter) with one typed method per genre, run as a TaskIQ job, writing a draft into the document row. Never runs unrequested; logs provider/model/tokens, never content. _Avoid_: conflating with Document intake (the pattern survives extraction being absent).

Claims evidence — QS tooling

Evidence pack
A persistent, attestable bundle of contemporaneous records over declared parameters (time window × area × contractor × record classes): a frozen context snapshot plus a branded narrative document and raw data annexes, generated by the report pipeline's provider DAG. At generation an append-only attestation records a content hash over pack + snapshot + parameters — "this pack, these contents, this moment". Permit windows/timelines serve as the in-system as-planned leg; blockers, audit, clash episodes, weather, and the stock ledger are as-built; the external P6 programme stays external. A Claim workspace (heads of claim, valuations, lifecycle) is deliberately not an entity. The pack workspace surfaces as a permission-gated tab of the Analysis suite — the QS's single front door — not a standalone dock panel (decided 2026-07-13, grill session). _Avoid_: claim (the QS's argument, made outside SimOops); report (the daily coordination artifact); export (an ephemeral download; packs persist).
Commercial (role)
A site role for the QS/commercial persona, minted in the hse pattern: sitewide read (cross-contractor by necessity), evidence-pack generation, export and audit_view — and no entity mutation or triage/workflow verbs. Exists so claims pilots never start with a coordinator over-grant. _Avoid_: QS role (persona, not the role name); quantity surveyor (ditto).
Completion record
A declared, evidence-backed statement that the work a token represents was done on a given site-local day: keyed by (entity, work_date) — one per token per day, which is what makes all-day (unwindowed) tokens completable day by day. Covers the temporal clashable kinds — those that live on the day axis, carrying start_at/end_at (Tokens, ActiveAreas). Not deliveries, whose completion-with-evidence moment is Receive; and not roads (nor buildings/zones), which are persistent geometry with no temporal columns at all — no work date names their work, so there is nothing to complete. Note the test is "has a time axis", *not* "has a non-null window": an all-day token has a null window and still completes. Living, not frozen: reversible and editable within the declaring contractor's authority (the require_contractor_access seam), with every change on an append-only event strip so contemporaneity stays provable for Evidence packs. Surfacing only — completing never mutates the token's window or geometry and never removes it from clash detection. Amended 2026-07-17: daily completion is optional and silent — no nudge, no visual guilt; the QS as-built takes whatever exists (Close-out (work sense), one of three gestures). Every record-work-completion gesture carries the Snag wee box. Capture is per token (properties panel, mobile record-completed-work) or via the site browser's multi-select bulk close — the My Work *Close out* tab (decided 2026-07-13) is retired with the panel's rename to *My Tasks*. Token expiry ≠ completion. Decided 2026-07-06 (grill session); road retired as an anchor 2026-07-15 (the axis is temporal, not clashable — D2 correction). See docs/decisions/2026-07-06-token-completion-records.md and docs/decisions/2026-07-17-close-out-two-verbs.md. _Avoid_: completed status (Delivery's enum value — a different lifecycle); close-out (the Permit close-out transition); sign-off (a possible future QS attestation, deliberately not this); "every clashable kind" (roads clash but never complete).

Permits to Work

Permit
A formal, time-bounded authorisation for work on a site: who (contractor), what (scope of work, controls), where (location), and when (a start/end window). One entity regardless of how it arrived — via the in-app request/approval workflow or via file import from an external allocation tool (provenance is recorded). A permit answers *"is this allowed to happen here?"* — it sits on top of the RAMS process, it does not replace it. _Avoid_: permit number (that is one field of a Permit, and historically the only vestige of one); PermitSet (the import batch record, not the permit).
Permit-linked entity
An on-map entity that can carry a permit_id — i.e. trace to a Permit. Five kinds today: Worker, Plant, Delivery, ActiveArea (the schedulable polygon areas — work_area / exclusion / laydown / HRCA) and ReferenceFeature (reference geometry — zone / road / building). This is the entity set the inline permit-request flow and the Permit coverage policy reason about.
Permit type
A per-site template defining a category of permit — its name and code (e.g. Hot Works/HW), the pre-checks a requestor must complete, whether it requires a parent permit, and its default duration. Sites set their own list (per HSG250 practice); a UK/EU default set is seeded and sites adapt it during onboarding. A single permit may carry several types at once (see Permit control category); the type catalogue is the vocabulary, a permit selects a set from it. _Avoid_: org-scoped template (permit types belong to the site, mirroring how site rules define the permitted-activity list).
Permit control category
The set of hazardous-activity controls a single permit authorises — a permit is *"Hot Work, Confined Space, General"*, not one thing. Each category is a Permit type drawn from the site catalogue; the permit carries a set. The set is the permit's identity for two purposes: it drives the clash engine (a Hot-Work category near a Confined-Space category conflicts) and the card's at-a-glance chips. The governing workflow regime (stages, parent requirement, extendability, pre-check checklist, duration) is derived from the set by most-restrictive/union rules — there is no single "primary" type. General is the near-universal base category and is visually de-emphasised. On import, the external tool's free-text *Control* column is the source of the set.
Permit hierarchy
A parent/child relationship between permits, expressed as a capability of the permit type system (requires parent), not a universal invariant. The UK default templates model the common principal-contractor convention: a General Permit (authorisation to work on site, mostly free-text scope) parents high-risk children (Hot Works, Confined Space, …) which inherit its location and contractor, carry their own parent-nested windows (not the parent's dates), and add type-specific controls. Sites that only permit high-risk activity simply use types with no parent requirement.
Permit family
A General (umbrella) Permit plus its single-issuer child permits — "two permits stapled together, both with their own issuers" — each running its own lifecycle, window, sign-off and authorised issuer, grouped under a shared parent_permit_id. The family is the structural answer to cross-discipline work (distinct authorised issuers); a lone permit carrying a Permit control category set is the degenerate "family of one" for same-issuer control bundles. The family is the clash unit — a token's clash categories are the temporally-gated union over its whole family (parent + open children), so decomposition never goes clash-blind. Parent transitions gate on child aggregate state (close-out blocked while any child is non-terminal; "work begins" blocked until children are live). Decided 2026-06-18 (grill session) — "P2-soft": families primary, multi-category re-scoped (not removed). ADR forthcoming (amends 2026-06-16). _Avoid_: multi-category permit as the cross-discipline answer (that is the same-issuer control bundle); reading "stapled together" as mere display grouping (each child is a real, independently-signed permit).
Permit attachment
A file (RAMS, drawings, isolation certificate, lift plan, photo) uploaded against a Permit as the evidence behind its declared controls and pre-checks. Bytes live in S3; a row carries the metadata, reusing the same storage substrate as incident/observation evidence. Attachments are editable only while the permit is a draft (or in corrections), and their identities fold into the permit's signed content hash — so each approval signature attests to the exact file set, and a post-approval swap is detectable. A new or revised document after that point means corrections (pre-issue) or a reissue. Each file may be tagged to an Expected-attachment slot; a slot marked *required* must be satisfied before the permit can be submitted. _Avoid_: "evidence" for a permit's files (that is the WorkGroup / incident / observation noun — a permit has *attachments*, even though they share the storage helper); treating attachments as mutable after submit (they freeze).
Expected-attachment slot
A per-Permit type keyed prompt for a document ({key, label}, e.g. Confined Space → *Rescue plan*) against which a Permit attachment is tagged (expected_slot_key). Slots union across a permit's category set, deduped by key. A slot is either advisory — a satisfied/unsatisfied checklist that gates nothing — or required, in which case it gates submission: a Permit cannot leave draft until every required slot has a tagged file. required ORs across the category set (required by any contributing type → required in the union) and is enforced by the required_attachments_present workflow guard on the submit transition, not by a parallel check. _Avoid_: "expected attachment" for the *file* (the slot is the prompt; the file is the Permit attachment); assuming all slots block submit (only required ones do).
Permit coverage policy
A per-site policy stating that every placed Permit-linked entity must trace to an active Permit. When enabled, unpermitted entities are surfaced (badges, noticeboard signals), not blocked at placement — paperwork lag must not break the map-first workflow. Disabled on sites that don't run general permits.
Permit lifecycle
The fixed state machine every in-app permit walks: draft (requestor-only visibility) → requested → endorsed (first approval: coordination check) → approved (second approval: H&S desk review — on the permit schedule but not yet live) → issued (the walk-down: issuer and receiver both sign; work may proceed) → closed (formal close-out signed off). Rejected is terminal and available at all three gates; each gate can also send the permit back for corrections (requestor edits and resubmits — the full chain restarts). Imported permits join the machine at *issued*.
Workflow definition
The per-site (optionally per-Permit type) data artifact that *is* a permit's Permit lifecycle — a versioned graph of State marker-tagged states and action-keyed edges (each carrying a Gate binding, guards, and effects), seeded with the default lifecycle and clone-to-customised. The test-only FSM Machine promoted to a runtime source of truth; admins compose it from a fixed registry of dev-authored guards/effects/handlers and never author logic. Each permit pins the workflow_version_at_creation it began under, so edits never strand live permits. Built as a domain-agnostic substrate (an Incident or Observation may adopt it later). Decided 2026-06-18 (grill session); ADR 2026-06-18-configurable-permit-workflow-engine.md. _Avoid_: state machine (the shape is now data, not code); workflow *instance* (a permit walking a definition is not the definition).
State marker
A member of the fixed semantic spine — the dev-owned vocabulary every consumer keys off *instead of* a literal status value, so a site may rename or reshape its Workflow definition states without breaking anything downstream. Permit markers: WORK_MAY_PROCEED, EDITABLE, VISIBILITY (requestor / contractor+review / public), CLASH_RELEVANT, EXPIRY_ALARM, TERMINAL, SPENT, WITHDRAWABLE. A state carries a subset; consumers ask markers.has(X), never status == "issued". The same move as Clash category (rules filter on category, not enumerated variants). Decided 2026-06-18 (grill session). _Avoid_: status (the configurable per-site *name* — a marker is the fixed *meaning*); flag (overloaded).
Gate binding
The per-transition declaration of *who may take an edge* in a Workflow definition: a permission band (from the fixed SiteRole set) + an optional Permit appointment requirement + optional specific users + a site-defined display label (e.g. "PtW Coordinator", "Safety Manager"). It is the Permit appointment mechanism generalised from issuer-only to any gate — and how "define roles and responsibilities" is met without custom per-site roles. Decided 2026-06-18 (grill session). _Avoid_: role (a binding composes a SiteRole band with appointments + a label, it is not itself a role); gate actor (names the who, not the binding rule).
Permit event
A row in a Permit's permanent transition timeline — one per lifecycle action (created, submitted, endorsed, approved, issued, countersigned, closed, expired, withdrawn, rejected, corrections-requested, extended, reissued). It carries the actor (nullable — *expired* is system-driven), a timestamp, an optional note, and JSONB metadata, and is the source for the activity feed, the PDF history, and the CSV export. The cryptographic gate attestations live in the separate signature ledger; a signing event links its signature for the content-hash. Mirrors the incident timeline. _Avoid_: conflating an event with a signature (every signature has an event, but submitted / expired / withdrawn / reissued events have none); the delivery-bus *OutboxEvent* (transient and pruned — not the audit record).
Permit close-out
The formal act that ends a permit: work finished or stopped, area checked (fire watch held after hot works), tools and barriers removed, sign-off recorded. Expired is the alarm state — end time passed with no close-out — and still requires a close-out to reach *closed*. Unclosed permits are the red-flag surface; a site gate can hold new issues until yesterday's permits are closed. _Avoid_: expiry as a synonym for closure (the external tool conflated them).
Permit issue (walk-down)
The distinct act that makes an *approved* permit live: the issuer physically verifies controls at the workface (fresh gas test, exclusion zone up, named personnel present) and both issuer and receiver sign in-app. The issuer can refuse — bounce to corrections or reject — if conditions aren't met. _Avoid_: issue as a synonym for second approval (approval is the desk decision the night before; issue is the morning act that goes live).
Permit appointment
The per-site, optionally permit-type-scoped record naming a user as an authorised issuer — the in-app analogue of the written Authorised Person appointment in UK practice (formal, named, scoped, not portable between sites). Appointments layer on top of existing site roles rather than being a role: a zone construction manager is *both* a coordinator and an appointed issuer. Role bands give the workflow defaults (request = member+, first approval = coordinator+, second approval = hse/admin); issuing always requires an appointment. The same mechanism is how a site configures *who* acts at each gate. _Avoid_: permit-issuer as a SiteRole (one-role-per-membership can't model it); PTW-coordinator (the external tool's name for the second-approval band).
Permit signature
An authenticated in-app action: verified user identity, timestamp, the permit content as signed, and the state transition, audit-logged and rendered onto the permit document. Signatures occur at issue (issuer + receiver), close-out, and approvals. No drawn-squiggle capture — an authenticated action is stronger evidence than a wet-ink lookalike.
Permit receiver
The named site user (a member of the receiving contractor, defaulting to the requestor) who accepts the permit at issue and countersigns from their own device, confirming they understand its limits and conditions. The permit goes live only when both issuer and receiver signatures land. _Avoid_: free-text receiver names (the countersign must be attributable).
Permit board
The site-wide live coordination view of permits: what's issued and live where, what's approved for tomorrow, what's expired awaiting close-out. The board is public within the site — its whole function is the fire marshal and the neighbouring subbie seeing live hot works at a glance — even though in-flight applications (draft → approved) stay scoped to the requesting contractor plus the review band. In SimOops the board is a dock panel and the map *is* its spatial half (linked entities locate permits).
Close-out gate
A per-site, contractor-scoped policy: a contractor with expired-unclosed permits cannot have new permits issued (approvals proceed; going-live is held, with the offending permits listed) until they close out. The in-app form of "most sites won't issue tomorrow's permits until yesterday's are closed". Default off.
Permit reissue
The default way a permit's life is prolonged: a one-click clone with fresh dates and a reissued from link to its predecessor, walking the full chain again — including a fresh walk-down. UK practice insists on this for high-risk work because the issue act embeds physical re-verification; "extending" paper is the mechanism for skipping it.
Permit extension
A per-type capability (off for every high-risk type in the UK default set; on for General): a requested new end time approved at the second-approval gate, stretching the window without re-issue. Exists for administrative parent permits where nothing physical needs re-verifying. Child permits never outlive their parent's window. _Avoid_: extending high-risk permits (that is a reissue, by design).
Permit spatiality
Permits have no geometry of their own. The entities working under a permit (workers, plant, deliveries, areas) carry the geometry and reference the permit; a permit's spatial footprint *is* its linked entities. Spatial permit semantics flow through them: clash rules see a linked permit's type as an entity attribute (hot-works token near solvent-works token), and a permit's authorised extent, where one exists, is a linked work-area feature. The permit row keeps only a textual location and the import resolver's spawn hints. The external tool's spatial columns map onto SimOops terms with a vocabulary inversion worth pinning: their *Zone* is a SimOops Floor (building level), their *Area* is a SimOops Building or Zone, and their *Location* is free-text within. The persisted field literally named zone_raw holds a floor, not a zone. _Avoid_: permit-as-map-object (two spatial truths that can disagree — the token in the corridor under a permit that says plant room 3); reading "zone" off an imported permit as a SimOops Zone (it is the floor).
Linked / unlinked permit
Whether a Permit has any entities referencing it via the permit_id FK. A linked permit has at least one linked entity, so it has a spatial footprint (see Permit spatiality) and lights up the map under the permit lens; an unlinked permit exists but has zero linked entities — no spatial footprint yet — and is the population a coordinator works through when placing tokens. The authoritative signal is the live FK count (linked_entity_count), never a denormalised tally. This is the permit-side mirror of the token-side Permit coverage policy (a token with no permit): both ask "is the permit↔entity link present?", from opposite ends. _Avoid_: "mapped" (collides with the background site map); "active" (the live window / ActiveArea / alerts); "allocated" (resource allocation); "located" (an imported permit is already located by its text/matched-feature while still unlinked).
Permit placement
Dropping the entities that become a permit's footprint. Two distinct surfaces, easy to conflate: the apply workbench builds a *draft's* footprint *before* submit (placement there is required — a draft needs at least one linked entity to be submittable) and routes each drop through the confirm-and-link path; board placement is the rapid-fire walk that drops tokens under an already-issued Linked / unlinked permit|unlinked permit (the population is imported permits awaiting tokens). Board placement is issued-only — a draft offers "Continue application" (back to the apply workbench), never a board place affordance — and is tools-island-driven: the normal Add palette chooses the kind case-by-case, limited to the linkable map-token kinds (worker, plant, area). The map auto-focuses each permit's matched zone/building as the walk advances (viewport only — never the scrubber, see Permit spatiality). _Avoid_: placing tokens under a draft from the board (its footprint belongs in the apply workbench); "armed toolset" (placement no longer derives a per-permit tool set — the tools-island Add palette is the chooser).
Permit window
A permit's start_at/end_at define a daily recurring work window across a date range — *"07:00–17:00 each working day from the 11th to the 17th"* — not one contiguous block running through the overnights. A six-day permit is six daily windows, not 144 unbroken hours. This is the interpretation the entity spawner already encodes (buildPermitScheduleOccurrences stamps the permit's HH:MM onto each calendar day in range), so visualisations and clash-overlap must read the window the same way. Whether a *physical hazard* (an open excavation, a created opening) persists outside the daily window is a separate, category-dependent question. _Avoid_: rendering or reasoning about a multi-day permit as a single all-day/all-span bar (its true footprint is the daily window repeated).

RAMS

RAMS document
The register identity for a contractor's RAMS: site, authoring contractor, title, reference number. Content is never decomposed (no risk-table or method-statement fields) — RAMS revisions carry the files. Permits link a RAMS document by nullable FK alongside the legacy free-text rams_reference; permit submission warns, never blocks, on an unreviewed / superseded / past-review-date RAMS. _Avoid_: rams_reference (the legacy string); attachment (the permit slot a revision file also satisfies).
RAMS revision
One immutable issued revision of a RAMS document: rev label, attached file(s), review/expiry date, and a single overall verdict — submitted → accepted | accepted_with_comments | rejected (the source's A/AC/R key; rejection sends the contractor back to upload a new revision). The current revision is derived (latest accepted), never a stored flag. The linked revision's identity folds into the permit content hash at submit, so an approval attests the exact RAMS it rested on. _Avoid_: version (template vocabulary); superseded as a status (it is derived).
RAMS review lane
A per-revision, per-discipline review record — discipline from a per-site registry seeded with the AFRY five (Civils, Erection Lead, Mech/Elec Supervisor, Commissioning Manager, HSE), plus reviewer, note, date. Lanes are the evidence trail and never individually gate — the overall verdict is the revision's status (on the real review sheet most lanes are blank). _Avoid_: approval stage (permit vocabulary); reviewer band (RBAC term).
RAMS briefing signature
A signature-grade attestation that a named person was briefed on and signed a specific RAMS revision: name + company (optionally a user id), timestamp, and the revision's content hash — captured either by a supervisor on the operative's behalf or self-served through a per-revision public capture link (QR; the Observation capture link pattern). Does not mint a user identity for drivers or visiting crews. _Avoid_: sign-on sheet (the paper artefact this replaces); permit signature (transition-coupled, a different mechanism).

Site map

Site-map capture
A single georeferenced image of the site at a point in time — an aerial/drone pass or a scanned site plan — stored as a SiteMap (calibrated, COG-tiled). A site accumulates captures over its life; uploading a new one never deletes the old. Each carries a captured_at (when the survey was taken, distinct from when it was uploaded). _Avoid_: bare "capture" unqualified — it is overloaded with the export/report *screenshot* sense; and "version", which implies a strict lineage the flat capture list does not have.
Canonical site map
The one capture a site is currently running on — the shared background every member sees by default and that reports/exports bake in. Distinct from a member's local view. Decided 2026-06-09 (grill session): the active background is promoted from per-user ephemeral state to shared, coordinator-published site state (Site.active_site_map_id, broadcast over WebSocket); selecting a capture in the Site Maps panel *publishes* it (coordinator-only; members are read-only, no personal-preview tier). Every change appends to a site_map_activations log, so "which capture was active at time T" has one answer — the activation with the greatest activated_at ≤ T. A later, opt-in Phase 2 surfaces *temporal* divergence: when the scrubber sits far from the canonical capture's captured_at, a non-intrusive banner offers to jump to the capture current at that moment. See docs/decisions/2026-06-09-canonical-site-map-and-provenance.md. _Avoid_: "active map" as a per-user notion — it is now site-canonical.

Temporal

Active period
The single canonical "when does this entity exist / when is it scheduled" range. Expressed as two NOT NULL timestamps start_at / end_at on Worker, Plant, ActiveArea, and Delivery. A concrete absolute range (may span midnight). Decided 2026-05-22 (grill session): - Every primitive entity kind — including high_risk_commissioning_area ("HRCA") — uses *only* the Active period model. HRCA has no special "daily recurring window" semantic; its distinctiveness lives entirely in clash rules. - Both bounds are NOT NULL. No null = "all-day", no null = "open-ended", no sentinel dates. Every entity has a concrete range. - Delivery's scheduled_start / scheduled_end columns rename to start_at / end_at for consistency (legacy from before the model was unified). - See docs/decisions/2026-05-22-active-area-table-split.md.
Shift
A site-level configuration row defining a recurring clock-hour window (e.g. "Day shift, 06:00-18:00"). Shifts are referenced by name from Permit imports and from the dashboard scrubber to seed default Active period values when an entity is created. A shift is *not* a temporal property of an entity — entities only carry Active periods.
Schedule group
A set of entity instances generated from one recurring-schedule definition, sharing a schedule_group_id — its members are Occurrences. Spans Worker, Plant, and ActiveArea; each member carries its own Active period. The group exists while it has ≥1 member and ceases when the last is deleted (there is no separate group row).
Occurrence
A single member of a Schedule group — one entity row covering one Active period. The "All N" count in the edit/delete flow is the group's occurrence count.
Reach
When editing or deleting a member of a Schedule group, the choice of whether the change applies to this Occurrence alone or the whole group — surfaced to the user as the "This one / All N" prompt. Decided 2026-05-29 (grill session): "reach" is the canonical name for this axis; the edit/delete flow resolves a reach, then applies the operation at that reach. See docs/decisions/2026-05-29-schedule-operation-dispatch.md. _Avoid_: scope (AnalysisRegion already lists "scope" on its own _Avoid_ list — it is the loose word that concept was sharpened away from; reusing it here would overload the glossary).

Areas and reference geometry

ActiveArea
A polygon that participates in clash detection *and* carries an Active period. Variants: exclusion, laydown, work_area, high_risk_commissioning_area (HRCA). All ActiveAreas have NOT NULL start_at / end_at. PR3 of the unified-temporal-model rollout landed 2026-05-23 — schedulable polygons now live in their own backend table active_areas (separate from Reference features), with sibling history tables active_area_versions and active_area_geometry_history. See docs/decisions/2026-05-22-active-area-table-split.md for the forking rationale.
Whole-floor area
An ActiveArea with placement_mode = 'whole_floor', created by a Floor-fill action rather than a freeform polygon draw. Its polygon is snapshotted from the building's polygon-with-holes at fill time and frozen thereafter — subsequent building footprint edits do not auto-propagate. A building_footprint_revision mismatch with the building's current revision surfaces a "Footprint changed since fill" chip on the panel with a "Regenerate" action to re-sync the group's polygons explicitly. Operationally indistinguishable from a freeform area on the map — clash rules fire per-row, floor-aware rendering filters identically. The provenance lives on the panel, not on the map.
Floor-fill
The coordinator-facing action that creates N Whole-floor area rows (× K recurrences) over selected floors of a building in one shot. Primary entry-point is a "Floor-fill area…" button on the building properties panel; power-user accelerator is the C → A → F keyboard chord. Both routes land on the same modal.
Floor-fill group
The set of Whole-floor area rows produced by one Floor-fill action, identified by a shared floor_fill_group_id UUID. Invariant: placement_mode = 'whole_floor' IFF floor_fill_group_id IS NOT NULL.
Reference feature
A polygon (or linestring) that's part of the site's static geometry but carries no Active period. Variants: building, zone, and road (the linestring case). After PR3 (2026-05-23) the `geometadata_features table is exclusively reference geometry — the schedulable polygons moved to active_areas. A DB-level CHECK constraint enforces the boundary: feature_type IN (unknown, building, zone, road)`.
Floor footprint
An optional per-level polygon on a building — carried inside the level entry itself (the elevation_m grow-the-dict precedent), so it versions and replays through the existing feature history with no new machinery. Sparse by design: a level without its own polygon inherits the union envelope; nothing is backfilled. Together with the per-level elevation interval this forms the implicit prism model — the deliberate ceiling of SimOops's 3D ambition (per-level 2D polygon × elevation interval; no Z coordinates, meshes, or 3D geometry types in storage). Level awareness lands at write time: placement/move derive building_feature_id + building_level from refined (point, level-polygon) containment — auto-set, overridable — while the clash engine stays FK-based and level polygons never enter predicates. Overlapping envelopes resolve by refinement first (level containment, then smallest area), with the reference-feature Disambiguation picker for genuinely ambiguous picks. Decided 2026-07-06 (grill session). See docs/decisions/2026-07-06-per-floor-building-footprints.md. _Avoid_: level geometry (ambiguous with entity building_level); floor plan (the per-level interior drawing — a different system, see Floor plan); 3D model (explicitly ruled out).
Floor plan
The per-(building × level) interior drawing that renders while Floor focus is active — backed by exactly one of two artifact kinds: a raster COG (image/GeoTIFF uploads, or DXF imports taking the raster fallback) or a vector artifact (the default for DXF imports: an immutable per-level GeoJSON of the drawing's linework, text, and hatch fills, resolved at-time through the same audit-trail pattern as the raster cog_url). One plan per building level, whichever the kind. Decided 2026-07-13 (grill session). See docs/decisions/2026-07-13-vector-floor-plans.md. _Avoid_: "vector layer" (collides with reference layers and MapLibre style layers — say *vector-backed floor plan* or *plan linework*); "drawing" (the DXF is the *source*; the floor plan is the derived artifact).
Plan slab
The theme-coloured fill rendered under a vector-backed Floor plan's linework, clipped to the level's Floor footprint (else the building envelope) with a subtle edge lift — the "inside the building" ground that keeps interior linework from reading as marks on the site imagery. _Avoid_: "background colour" (the raster-era equivalent, baked into the COG at import; the slab is a live style layer).
Ghost linework
Floor-plan content falling outside the Floor footprint — grid lines, reference annotations — rendered muted/translucent rather than clipped, so it reads as reference floating over the site, not as geometry claiming the ground. Split from inside linework geometrically at extraction (footprint intersection), never by DXF layer-name convention. _Avoid_: "clipped"/"cropped" (the raster-era cutline truncated this content; the vector path deliberately preserves it).
Schedulable
Retired 2026-05-23 (PR3). Was a deprecated UI gate (isSchedulableType) listing exclusion/laydown/work_area as types that went through the permit-driven, datetime-bound creation flow. After PR3 every ActiveArea type is schedulable by virtue of living in active_areas (which enforces NOT NULL temporal bounds at the column level); the gate is gone.
Geometry cut
Subtracting a shape from an ActiveArea or Reference feature's polygon, leaving a hole (donut) or a reduced outline — recorded as a first-class operation (before/after snapshots, provenance, undo-by-restore) in the per-family geometry history. Two flows share one seam: the automatic feature-vs-feature cut (a building cuts zones/laydowns per the LayerType rule matrix — shipped) and the freeform cut (the user draws an ad-hoc cutter against a selected polygon; the cutter geometry is stored inline on the history row, gated by entity_edit, never by the rule matrix). Decided 2026-07-06 (grill session): a cut is also a *versioned event* — it writes the same temporal version rows as any other geometry update, so the scrubber and at-time reconstruction see it; every reshape path (vertex edit, PATCH, drag, undo) must carry interior rings through rather than collapsing them; cut-undo is a command in the main per-kind undo stack, with redo. See docs/decisions/2026-07-06-freeform-hole-cutting.md. _Avoid_: hole tool (the cut is the operation; a hole is one outcome); clip/boolean subtract (GIS jargon); erase (implies deletion of the entity).

Reports

ReportScene
A single clash-focused frame in a safety report — a geographic bounds (optionally a rotation-aware polygon + draw-time view), the entities spatially inside it, and a clash summary. The unit a coordinator accepts/rejects in the report wizard; each accepted scene is captured to one headline image. A scene *projects to* an AnalysisRegion but is not one. _Avoid_: snip (that's the capture mechanic), cluster (the pre-acceptance proximity grouping, not the accepted frame).
Site overview
The whole-site context image in a report — one full-page map of the entire site. Distinct from the per-scene minimap inset (the small locator thumbnail composited into each ReportScene image) and from a ReportScene image itself.

Analysis

Analysis tool
A *kind of computation and presentation* run over an AnalysisRegion. The founding tool is Region evolution — the ChangePointStream stepper/list; the suite now also hosts the Planner and Evidence pack tabs (a density *Heatmap* remains anticipated, shown disabled). A region-based tool consumes the same region a Constructor produces but renders differently and owns its own controls (the look-forward/look-back *horizon* slider belongs to the Region-evolution tool, not to the panel globally); the Planner and Evidence tabs embed their own surfaces rather than a constructor-built region. In the dashboard the active tool is chosen by a visible tool switcher; Constructors sit beneath the region-based tools and are shared across them. _Avoid_: mode (overloaded with Interaction mode and the create/select keyboard modes), analysis type (vague), constructor (that is the region-selection axis, not the computation).
Constructor
The UI affordance that *builds* an AnalysisRegion — how the user chooses the spatial scope. The grid offers scene_box (drag a box), entity_neighbourhood (anchor on an entity + knobs), building (pick a building + floor(s)), zone (pick a zone feature), and freehand_polygon (draw a polygon). All but entity_neighbourhood differ *only* in how the polygon_wgs84 + level_set are obtained; the frontend resolves each to that shared shape and the AnalysisEngine is identical across them. The anchor_kind discriminant records provenance for display/audit, not engine behaviour. _Avoid_: tool (the computation axis), anchor (fits entity_neighbourhood specifically — a drawn box has no anchor), selector (generic).
AnalysisRegion
A scoping value: *which entities and which time window* the AnalysisEngine should consider. A tagged union over anchor kinds — scene_box | entity_neighbourhood | building | zone | freehand_polygon — each variant carries the inputs its anchor needs. Every variant carries a time_window (Active period applied to the analysis: forward_days/backward_days relative to the scrubber's viewDateTime) and the polygon-plus-level-set that the engine ultimately queries against.
AnalysisEngine
The pure function that takes an AnalysisRegion and produces a ChangePointStream for the region's time window. Uniform on the region's polygon, not polymorphic on anchor kind: every Constructor resolves to a polygon_wgs84 + level\_set on the frontend (including entity_neighbourhood, whose knobs are resolved via turf.js), so the engine always computes the same thing — "entity-id set inside polygon × level\_set changes". The anchor_kind is carried for provenance/display only; the engine does not branch on it.
ChangePoint
A single unit of inflection emitted by the AnalysisEngine. Carries:
ChangePointStream
The ordered list of ChangePoints returned by the AnalysisEngine for one AnalysisRegion over its time window. Always anchored at the scrubber's viewDateTime; emits points within [viewDateTime - backward_days, viewDateTime + forward_days]. The stream is computed on demand — no precomputed materialised view — and cached by (region-hash, viewDateTime, anchor_id) only within the panel session. _Avoid_: timeline (overloaded with gantt timeline, audit-log timeline, scrubber timeline), inflection stream (jargon), change-feed (overloaded with WebSocket "feed" terminology).
Stepper
The UI control inside the analysis panel that drives sequential navigation through a ChangePointStream. "Next" / "Previous" buttons (plus arrow-key chords when the panel is focused) advance the scrubber's viewDateTime to the next/previous ChangePoint and expose the delta summary in a banner. The same panel surfaces the stream as a list so the coordinator can jump to any ChangePoint directly; sequential and jump-to are alternate projections of one stream.
Region evolution
The look-forward/look-back Analysis tool — it walks a ChangePointStream over an AnalysisRegion. One tool, two *surfaces*: live in the dashboard Analysis panel (the Stepper + change-point list) and in-report as a section. The report section consumes a ChangePointStream for a ReportScene's projected AnalysisRegion and renders each ChangePoint as a row in a per-region block; coordinator ticks which rows should be captured as a full map screenshot at export time. Unticked rows render as table-only entries (timestamp, delta summary, clash counts). The lookahead is opt-in per scene — adding a scene does not implicitly enable a Region evolution block for it.

Planner analysis

Planner
The persona this capability serves: a user who owns a longer-term master schedule (WBS / activities in P6 or similar) held outside SimOps and uses SimOps as a representation of site reality to check that plan against. Whether "planner" becomes a distinct SiteRole or is served to an existing role is unresolved (RBAC TBD). _Avoid_: scheduler (overloaded with the recurring-Schedule group machinery), coordinator (a distinct in-app persona/role).
Reality (planner sense)
What Planner analysis treats as ground truth: the live, as-recorded operational state — issued Permits together with placed tokens (Workers, Plant, ActiveAreas, deliveries) — read on the valid-time axis (each entity's Active period: *when work is scheduled*, the column the scrubber walks), not the system-time edit history. It is a *proxy* for reality, not verified actuals: the rows are mutable and only as current as the site team keeps them. Because the read is valid-time, the V1 views need no new history/versioning machinery — the live DB already encodes the whole schedule, past to future. Distinct from the external master plan and from the planning-cycle plan_state='actual' fork (removed in #674). _Avoid_: actuals (implies verified/frozen — these are mutable live rows), ground truth (overclaims), plan (the opposite — the external expectation).
Activity (planner sense)
The atom the planner reads and compares: an issued Permit seen as a unit of scheduled work — the closest SimOps analogue of a P6 activity, since it alone carries a structured *who* (Contractor), *what* (permit categories), *when* (Permit window) and *where* (matched Named place + linked tokens). Placed tokens are its manifestation: they supply the hard location (their building/zone FKs) and the human detail (label, worker_type) the permit's own free-text location lacks. Tokens carrying no permit surface as a separate "unpermitted" bucket. _Avoid_: task (overloaded with the Task action item), WBS element (the external plan's unit), activity-as-a-new-entity (it is the permit under a planner lens, not a new row).
Named place
The unified location axis for Planner analysis: a building or a zone (the site's two non-temporal named Reference feature kinds), treated as one "place" axis because a planner reasons about both as named locations ("Electrical Building", "Turbine Hall") whether or not SimOps gives them floors. Floor (a building's building_level) is a first-class drill-down — "in the Turbine Hall" and "on Level 2" are both valid queries. A place is attributed from the tokens' hard FKs (building_feature_id, zone_id), with a permit's free-text location as fallback. Vocabulary inversion (see Permit spatiality): the planner's external terms are the *inverse* of SimOps's — their *Zone* = our floor, their *Area* = our building/zone. V1 renders SimOps's terms; a translation adapter is deferred. _Avoid_: zone (one member, not the axis), area (overloaded with ActiveArea), location (too generic).
Planner timeline
The spine of Planner analysis (working name): a lookahead-shaped activity matrix — rows grouped contractor-first (Named place as filter / sub-group, pivotable to place-first), columns a near-term window, cells the Activity (planner sense) bars (the Permit window daily-recurring semantics preserved). Built to structurally mirror the planner's P6 Gantt so the two read side by side. Decided 2026-06-18 (grill session): - A dedicated Planner surface, not a tool inside the coordinator Analysis panel (see Analysis tool): the postures differ (broad, contractor-organised, lived-in vs region-scoped, ephemeral, scrubber-stepped). It reuses the AnalysisEngine + AnalysisRegion / Constructor model and the permit-ribbon *math* (permitDailyWindows), rendering fresh — the permit-board ribbon's own today | tomorrow × intraday-hour view is the wrong granularity to stretch. - Lookahead time model: default ~14-day horizon (the 2-week short-interval lookahead) + a ~5-day backward tail (so overstaying — "still here 3 days after they should have moved" — is visible), expandable to ~6 weeks; day columns, week columns when stretched. - Now-anchored and scrubber-decoupled: its own anchor-date control; it does not re-window when the map scrubber time-travels; selecting an activity/cell drives the scrubber one-way for spatial inspection. (A Now-anchored surface in spirit.) _Avoid_: Gantt (the shape, not the thing), Lookahead (as the abstraction title — the surface is bidirectional via the backward tail; "lookahead" is fine as a user-facing *label*, echoing the Region evolution avoid-note), permit board (the operational Permit board ribbon — a different surface).
Congestion (trade-stacking)
The headline spatial-temporal metric of Planner analysis: the degree to which a Named place is over-subscribed over an interval — the count of distinct Contractors (and/or total token footprint against the place's capacity) co-located there. It is the insight SimOps can surface that the planner's external schedule cannot: P6 encodes activity *logic* but not *spatial* contention, while SimOps is spatial-native. Drives the aggregate mode of the place choropleth (see Planner timeline) and the congestion-over-time companion ribbon. _Avoid_: clash (the Clash engine's proximity/rule conflict — congestion is a planning density, not a safety clash), crowding (informal), density (the continuous-heatmap *rendering*, not the place-level metric).
Resource loading
The reality-side resource histogram: headcount (and plant count) per Contractor per period across the Planner timeline window, rendered as a ribbon beneath the timeline (the WeatherRibbon SVG pattern reused). The planner lays it against their external resource-loading chart. Purely descriptive — no planned-vs-actual baseline exists in-app (see Reality (planner sense)). _Avoid_: S-curve (cumulative-progress — not computable without a baseline), utilisation (implies a capacity denominator SimOps does not hold).
Dwell time
The consecutive duration a Contractor has occupied / is scheduled in a Named place — the metric that operationalises *overstay* detection inside the present-reality constraint: the surface shows "in the Electrical Building N days and counting, M scheduled" (the Planner timeline's backward tail supplies the "and counting") and the planner judges whether that is wrong. Surfaced as a per-Activity (planner sense)|activity badge plus a sortable column. The app never asserts lateness; it supplies the number. _Avoid_: duration (the Active period length of one entity — dwell aggregates presence across a place), age (overloaded).

Site provisioning

Create-Site wizard
The in-app flow that provisions a new Site and its operable baseline. Superadmin-only today (architected so the gate can widen later). Builds progressively: step 1 creates the bare Site row + the creator's verified admin SiteMembership, and each later step drives the *existing* per-site operations against that real site — shifts, a first Contractor, the active rule profile, the background Site map, and optional invites.

Site organisation — contractors

Contractor
A team operating on a Site; its members own the workers, plant, deliveries, areas, and permits tagged to it. A Contractor belongs to exactly one Site.
Main contractor
The single principal Contractor of a Site, designated per-site and opt-in — a Site may have none. When set it is the root of the contractors-panel hierarchy and the parent of every otherwise-unparented Contractor on the Site.
Contractor oversight
A supervisor → subordinate edge between two Contractors on a Site: the supervisor's members gain CRUD parity over the subordinate's entities. The edge does double duty — it is *both* the authorisation grant *and* the panel hierarchy. "Appears under X" ≡ "X can edit it".
Transitive authority
Whether Contractor oversight authority cascades down the subtree. When a Site's oversight_transitive flag is on (the default), a supervisor's members reach *every* descendant — so the Main contractor reaches every Contractor on the Site; when off, authority is one-hop (direct subordinates only).
Subordinate / supervisor
The two ends of a Contractor oversight edge — the subordinate is nested beneath, the supervisor sits above. In UI prose "responsible" is an acceptable synonym for supervisor. _Avoid_: parent/child (reserve those for generic tree mechanics).
Oversight relationship
A viewer-relative classification of a clash-token (worker, plant, or ActiveArea — anything entity_kind: token) by where its owning Contractor sits relative to the viewing user's Contractor: - mine — the token's Contractor *is* the viewer's; - subordinate — the token's Contractor is in the viewer's canonical supervised set (the Subordinate / supervisor subtree, honouring the Site's Transitive authority flag — *not* an unconditional descendant walk); - other — everything else, deliberately absorbing supervisors, unrelated Contractors, and unassigned (no-Contractor) tokens alike.
Relevant clash
A viewer-relative classification of a Clash: a clash is relevant to a viewing user when at least one participating entity is owned by a Contractor in the viewer's responsibility set — their own Contractor plus its Subordinate / supervisor subtree (honouring the Site's Transitive authority flag); equivalently, any participant whose Oversight relationship is mine or subordinate. A clash whose participants are *all* other is non-relevant.
Affiliated coordinator
A user whose Site membership carries a coordinator/admin role *and* a contractor_id. They are vantaged (their Contractor anchors an Oversight relationship vantage), so they receive the Relevant/Other panel grouping like any contractor — but their Clash stance authority follows their role (coordinator = site-wide), not their Contractor scope. Clash grouping (vantage) and stance authority (role) are deliberately decoupled; an affiliated coordinator is the case that separates them. Decided 2026-06-05 (grill session); see the ADR. _Avoid_: conflating "what I'm responsible for" (vantage/grouping) with "what I may act on" (role/authority) — they are independent axes.

Demo sandbox

Demo Template
The single, PII-free snapshot of the real SCEC site from which every Demo World is cloned — produced once by Scrub (demo sense)|scrubbing a real SCEC extract, and refreshed only when the schema or seed data changes. _Avoid_: "golden copy", "demo seed" — it is not the synthetic site built by seed.py; it is a scrubbed clone of the *real* site.
Demo World
An ephemeral, isolated, throwaway instance of SimOps preloaded with the Demo Template, handed to one prospect to explore freely; independent from every other Demo World and discarded when it goes idle or ages out. _Avoid_: "demo session" (collides with the auth/HTTP sense of session and the unrelated ReportSession entity); "demo site" (a Demo World *contains* a site, but is the whole isolated instance, not just the Site row).
Scrub (demo sense)
The one-time transformation that removes all PII and commercially-sensitive content from a real SCEC extract to produce the Demo Template — by dropping, replacing-with-synthetic, or redacting each field per a scrub map. _Avoid_: "anonymise" — scrubbing also drops commercial IP and may relocate identifying geometry, not only personal identifiers.
Synthetic floor plan
A procedurally-generated, function-themed building-interior raster that stands in for a real Floor focus|floor's plan in the Demo Template, so the demo never exposes a client's actual architectural drawings — one per building floor (and the storage zone), georeferenced to the real footprint and rendered in a shared paper-and-ink CAD style. Amended 2026-07-13 (grill session): with vector-backed Floor plans, the stand-in matches the real plan's kind — a synthetic vector twin (the same procedural linework emitted as GeoJSON) for vector-backed plans, the raster for raster-backed ones. _Avoid_: "fake plan", "placeholder" — it is a deliberate, footprint-accurate stand-in, not a blank or a generic filler.
Character
One of the named, fictional people baked into the Demo Template as the demo's cast — each holding a genuine role-scoped site membership (coordinator, HSE manager, contractor supervisor, QS, site manager, client), except the anonymous operative, who deliberately has no account. Every Tour is experienced *as* a Character; the visitor can switch between them; workflow counterparties are other Characters. Fiction by construction — injected after scrub verification, never derived from a real person. _Avoid_: "persona" (this glossary uses persona for the abstract user archetype a capability serves; a Character is a concrete demo identity), "demo user" (ambiguous with the visiting prospect), "actor" (reserved for Co-actor).
Co-actor
A Character other than the one the visitor is viewing as, performing its side of a workflow while the visitor watches the consequence arrive live — the contractor's photo close-out landing in the HSE review queue. How one visitor experiences a two-sided loop without switching seats mid-story. Co-actor actions are real domain writes in the visitor's own Demo World. _Avoid_: "bot", "ghost", "simulation" (nothing is mocked — the writes are real).
Re-anchor (demo sense)
The Demo Template transformation that shifts every timestamp in the scrubbed extract so the snapshot's "present" lands on today, preserving site-local wall-clock times — making a Demo World's data genuinely current instead of frozen at the extraction date. _Avoid_: "rebase" (collides with git and with the Alembic migration-chain rebaseline), "date shift" (imprecise about the wall-clock-preserving contract).
Fabricated history
Deterministic fictional records — stoppages, ledger events, inspection runs, review chains, investigation timelines — written into the Demo Template after scrub verification, giving the record classes the scrub necessarily drops a believable past. The substrate every history-selling demonstration stands on. _Avoid_: "synthetic data" (too broad — synthetic floor plans are a different thing), "backfill" (collides with data-migration backfills).

Learning surfaces — tutorial & onboarding

Lesson
The atom of teaching: everything we teach about *one addressable thing* — a tool (Worker, Box-select), a panel (Noticeboard, the permits board), or a concept that has no single button (clash detection, the timeline scrubber, Floor focus). Keyed to that thing's id in a tool/feature registry. Concept-Lessons (no backing control to point at) are first-class — the reference library needs a "what is a clash?" entry even though there is no clash *button*. A Lesson holds up to two *renderings* of the same teaching: a Clip and a Recipe. Authored once; composed by many Tours and by Contextual help. _Avoid_: "module" (overloaded — names the unit but also any code module), "topic", "card" (Briefing already owns "card"), "step" (a step is one beat *inside* a Recipe, not the Lesson).
Clip
The reference rendering of a Lesson: a short, authored, autoplaying screen-capture (+ blurb) that shows the thing in ~10 seconds. The *pull* artifact — what Contextual help surfaces, and the medium of the per-tool reference library. Produced by the docs-site capture pipeline (provenance- hashed, staleness-flagged). _Avoid_: "video" (a Clip is a short silent loop, not a narrated film), "screencast" (implies length + voiceover).
Recipe
The tour rendering of a Lesson: the live step(s) that teach the thing by making the user *operate the real app* — what to spotlight, what outcome to wait for, what consequence to point at once it happens. The *push* artifact — what a Tour sequences. A Recipe drives and observes the live app in the user's own Demo World (or real site); it renders nothing pre-recorded. _Avoid_: "script" (overloaded with JS), "flow" (collides with the capture pipeline's Playwright flows), "steps" alone (a Recipe is the named bundle).
Reveal beat / Do beat
The two kinds of step a Recipe is built from. A reveal beat — the app points the user at something that has appeared (the now-red token after a clash, the clash row in the Noticeboard) or narrates over a dim, while the user watches. A do beat — control passes to the user; the beat completes when the user *achieves the beat's outcome*, however they get there (gating is on observable domain state — "a clash now exists", "a pin is selected" — not on a prescribed click path). A Tour interleaves both. _Avoid_: "show step / action step" (weaker), "task" (Task is the surfacing concept), "interaction" (every beat is an interaction of some kind).
Tour
An ordered, audience-targeted sequence of Lessons plus connective narration — a *playlist*. Owns no teaching content of its own; it sequences and frames Lessons' Recipes. The prospect Tour is one ("spatial basics → move a worker → watch it clash → wind the clock → read the noticeboard"); a "permits deep-dive" is another; a role-specific customer-onboarding Tour is another. The same Lesson may appear in several Tours. _Avoid_: "walkthrough", "journey" (buzzword), "guide" (the docs-site Guide zone owns that word), "wizard" (implies a one-off form-completion flow).
Contextual help
The pull surface: surfaces a *single* Lesson's Clip (+ blurb) right where its tool lives, on demand — reached by a deliberate affordance on the tool itself. No sequence, no narration, no app-driving — just "what is this thing", summoned when the user wonders. The reference half of the system; the Tour is the guided half. _Avoid_: "tooltip" (a tooltip is a one-line label; Contextual help opens a Clip), "help mode", "inspector".

Role handbooks — permission-derived induction

Handbook
A per-role induction document, derived from the permission spine rather than hand-maintained: one book per distinct ROLE_PERMISSIONS frozenset (viewer and client merge into the read-only book while their sets stay identical). A book's reachable-feature skeleton is generated from the Permission model and stitched into a workflow narrative by authored connective tissue, so a permission change mechanically updates the book and flags the stitching for review. Public: a handbook describes the product's role model, nothing site-specific. _Avoid_: "role guide" (the Guide zone is the end-user help; a handbook is the role-scoped induction read), "manual".
Band
The coarse capability class a role holds on a feature — one of view, contribute, manage, approve, in ascending order. Derived mechanically from the role's permission verbs on that feature (the highest verb class held), it is the one-line summary a Handbook shows against each feature; the full all-roles × all-verbs matrix is the internal reference on the feature page. A band change is pinned and queued for stitching review, never silently re-pinned. _Avoid_: "level", "tier" (a tier is the documentation quality contract, a different axis), "role".
Placement
The unit of authorship in a Handbook: a ::: feature <row> block whose head the generator fills (feature name + Band line + link) and whose body is the authored connective tissue. Its position in the section tree *is* the workflow ordering. A placement naming a feature the book's role cannot reach breaks the build; a reachable tier-1 feature left unplaced reddens the coverage report; a reachable tier-2 feature left unplaced is swept into the book's "Also available to you" appendix. _Avoid_: "section", "entry" — a placement is specifically the feature block, tissue and all.
Generated from source · SimOops living system reference