Files
shd.build/docs/architecture.md
T
jb 7ca6cb8458
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 33s
Validate and deploy staging / deploy (push) Successful in 1m9s
Updating mechanics for OS protocols
2026-08-03 16:12:20 -04:00

1088 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SHD.build architecture
The team builds the project in vertical slices.
## Application stack
- SvelteKit 2 / Svelte 5 with TypeScript
- Embedded Turso Database via `@tursodatabase/database` for durable application
data
- Auth.js-compatible server authentication with Discord OAuth and credentials
providers
- Argon2id password hashing, server-side sessions, CSRF protection, and role
checks
- Browser `localStorage` only for anonymous build drafts
## Product boundaries
- Public: browse, sort, and view published builds and guides.
- Anonymous: create and edit local drafts.
- Authenticated: publish, rate, comment, favorite, copy, and manage owned
builds.
- Editor: manage builder source data.
- Admin: manage users, roles, source data, and audit history.
Server code checks authorization for each mutation. UI visibility has no effect
on server authorization.
Builder and shared Build pages currently expose only deterministic totals from
the equipped items, selected rolls, fixed bonuses, inherited profiles, augments,
and active set bonuses. Conditional Talent outcomes, weapon and Skill DPS,
encounter assumptions, activation profiles, rotations, and simulation remain
planned. Explicit `COMING SOON` panels identify those player-facing
capabilities.
Weapon and gear range calculations must preserve decimal endpoints and follow
the tested progression rules in
[`STAT_LEVEL_SCALING.md`](STAT_LEVEL_SCALING.md). Leveling is linear rather than
compounded. The presentation layer truncates game-matching integer values.
Local credentials use Argon2id PHC strings with a unique random salt per
password. Current work factors meet OWASP's 19 MiB / 2 iteration / single-lane
minimum profile. Successful login rehashes credentials with outdated parameters.
## Database runtime
The application uses Turso Database in-process through its native Node package.
It opens a local SQLite-compatible database file. The application does not use
Turso Cloud, libSQL HTTP, remote replicas, auth tokens, or synchronization.
The database defaults to `./data/shd-build.db`. Production deployments must
provide a persistent writable filesystem and can override the location with
`TDR_DATABASE_PATH`. This rules out ephemeral serverless filesystems unless a
deployment attaches a persistent volume.
## Site launch controls
The singleton `site_launch_settings` row stores the public launch gate, account
creation state, and availability for News, Database, Builds, Guides, and
Builder. Schema migration creates the row with Coming Soon enabled, account
creation enabled, and all five sections enabled. The application caches the row
for five seconds and clears the process cache after an operator saves changes.
Agents with `site.settings.manage` edit the row from ISAC → System Configuration
→ Site Controls. The save action requires an explicit value for all seven
controls and a confirmation token. A successful update records
`system_configuration / site_access.updated` with the operator, target row, and
complete before/after snapshots.
The request hook applies the launch gate to anonymous traffic and section
availability to every request:
- With Coming Soon enabled, anonymous `GET` and `HEAD` requests redirect to
`/coming-soon`. Anonymous mutations outside exempt routes receive a JSON 404.
- Authenticated agents bypass the Coming Soon gate and enter the application.
- A disabled public section blocks its route family for every user, including
authenticated agents: `/news`, `/database`, `/builds`, `/guides`, or
`/builder`. `GET` and `HEAD` requests redirect to `/coming-soon` with the
unavailable section identified. Mutations receive a JSON 404.
- Authentication callbacks, sign-in, contact, legal pages, health endpoints,
scheduler endpoints, application assets, and the prelaunch page remain
reachable without a session.
The global navigation omits disabled sections for every user. Server checks
remain authoritative for direct URLs and mutations. An authenticated operator
may open `/coming-soon` to preview the promotional page, but the preview does
not override section availability.
Account creation uses a separate control. Closing registration blocks password
registration, a magic-link request for an unknown email, and Discord OAuth for
an unknown Discord identity. Existing password, email, and Discord identities
can still sign in. The sign-in page hides the password registration form and
explains registration closure. This guard runs on the server, so a crafted form
submission or OAuth callback cannot create an account while registration is
closed.
## Community content moderation
Comments, profile biographies, custom avatars, and Guides share the moderation
lifecycle documented in [`CONTENT_MODERATION.md`](CONTENT_MODERATION.md). Local
rules and the configured provider classify each submission before publication.
Clean submissions can publish without human work. Flagged submissions and
fail-closed provider errors enter the appropriate Oversight queue.
Agents may report visible content they do not own. Reports enter Oversight
without hiding the content. Human decisions require a reason and append an audit
event. Long-form edits that need review use immutable pending revisions so the
last approved biography or Guide remains public.
ISAC Site Controls owns provider behavior, content scopes, thresholds, link and
PII checks, and allowed or banned terms. API credentials remain in server
environment configuration.
## Guides and News articles
`articles` stores the stable identity, kind, owner, immutable slug, visibility,
and published or pending revision pointers for Guides and News. Authors edit one
mutable `article_drafts` row with optimistic version checks. Publication creates
an immutable `article_revisions` snapshot. Public readers and search load only
the revision named by `articles.published_revision_id`.
Guides require OpenAI moderation before an unlisted or public revision can
publish. The moderation request includes the title, deck, document text, image
alternative text and captions, cover image, and inline images. A failed provider
request stays in Guide Oversight. Clean revisions may publish automatically;
flagged revisions preserve the previous public revision while Oversight decides.
News bypasses this lifecycle because `news.manage` is the editorial gate. News
comments still use community moderation.
### Ubisoft News syndication
The Ubisoft News importer reads the English-language Division Resurgence feed
from Ubisoft Nimbus. It stores each Nimbus item ID as the stable external ID and
publishes the source title, excerpt, canonical Ubisoft link, and validated
Ubisoft-hosted thumbnail. SHD.build does not copy the article body or image
file. A bounded same-origin endpoint proxies each thumbnail so opening the News
listing does not send visitor metadata to Ubisoft. News cards link to the
canonical Ubisoft article. Imported articles use Ubisoft's original publication
timestamp rather than the import time.
`external_news_sources` maps each Nimbus ID to one SHD.build article. A SHA-256
content hash covers the normalized title, excerpt, canonical URL, and source
publication timestamp. An unchanged hash updates `last_seen_at` without creating
a revision. Thumbnail changes update the audited source metadata without
creating an article revision. A changed content hash creates and publishes an
immutable revision. The importer does not delete or unpublish an article when
Nimbus omits it from a later feed, and it does not restore an article that an
editor deleted. The Newsroom treats source-linked articles as read-only so local
edits cannot retain Ubisoft attribution. The importer defers future-dated items
until their source publication time.
The seeded, non-public `Ubisoft Official News` service identity owns imported
articles and acts on their audit events. A first import records
`editorial_content / news.imported`; a later content sync records
`editorial_content / news.import_synced`. Audit snapshots identify the provider,
Nimbus ID, canonical URL, thumbnail metadata, source timestamp, and resulting
revision.
The existing bearer-authenticated scheduler endpoint invokes the importer. A
successful run sets the next check one hour later. The importer stores the last
attempt, last success, next check, consecutive failure count, and bounded last
error in provider state. Failures retry after 15 minutes with exponential
backoff capped at six hours. A successful run clears the error and failure
count.
The article document is a versioned, strictly validated Tiptap JSON envelope.
The application stores no author-supplied HTML and renders accepted nodes with
Svelte components. `article_revision_build_embeds` supports several Builds per
Guide and several Guides per Build. Each relation records a bounded publication
snapshot. Authors may embed any Build they own; another Agent's Build must be
public and becomes unavailable in the Guide if its source stops being public.
Guide taxonomy combines fixed activity and difficulty terms with active
Specializations, Focuses, and weapon categories. `article_search` stores the
published title, deck, body text, and tags. Weighted token scoring prioritizes
title, deck, tags, then body because the embedded Turso engine does not ship the
FTS5 module. Search rows exist only for current public revisions.
Registered and anonymous visitors may rate a Guide from 1 through 5. Anonymous
ratings and Guide reports use a random HTTP-only browser token; the database
stores only its domain-separated hash. Repeated anonymous activity requires a
Turnstile result. Guide reports reference the exact published revision so a
report against an older revision cannot hide newer work.
The Builder stores loadout data and displays deterministic equipped-item totals.
It has no Guide body. Shared Build pages use the same player-facing calculation
boundary. An owned Build page links to `/guides/new?build=<id>`, where the
server verifies ownership and initializes a removable Build block in a new
private Guide.
### Discord-compatible Build sharing experiment
Persisted public and unlisted Builds expose absolute Open Graph metadata for
their canonical `/builds/[slug]` URL. Their `og:image` and Twitter card metadata
reference the same-origin 1200 × 630 PNG at `/builds/[slug]/preview.png` so
Discord and other link unfurlers can render a Build summary. Private Builds set
`noindex,nofollow`, expose no crawler image, and provide no usable sharing
control.
The preview endpoint derives its image from canonical Build facts and the
current known-stat summary in the shared Build projection. It includes the Build
title, author, visibility, direct URL, equipped weapon names, core totals, and
up to three supporting Attributes. The renderer performs no external fetches. It
normalizes and bounds every text field by grapheme count, escapes text before
inserting it into XML, builds a text-only SVG, and rasterizes that SVG to PNG
with Sharp. Production installs DejaVu Sans from the Debian package and the
renderer names that fixed family explicitly.
The endpoint hashes a versioned serialization of the image input for its bounded
in-process render cache and derives the HTTP ETag from the resulting PNG bytes.
A bounded request queue limits concurrent database projections and Sharp work.
Responses require cache revalidation so a visibility change to Private blocks
the next request instead of leaving a stale response window. Matching
conditional requests receive `304 Not Modified`. Private, missing, and
inaccessible Builds receive no preview. Unlisted images send the
`X-Robots-Tag: noindex` response header.
For eligible Builds, the Build Uplink invokes the native Web Share API as the
primary action when the browser supports it. An explicit Copy Link action
remains available for every public or unlisted persisted Build. This experiment
does not install a Discord bot, post to Discord, extend Discord OAuth scopes, or
store Discord tokens or webhooks. Discord receives only a link that an Agent
chooses to share and may fetch the public metadata and same-origin preview.
## Public data corrections and discussion
Every public database card links to its canonical ISAC record with an exact
`item` ID and offers a moderated Community Discussion. Authenticated Agents can
post notes and replies; Community Oversight may pin one visible comment as the
record's Community Note. A public correction opens the same ISAC editor used by
data operators. Contributor-mode submissions create a proposal rather than
changing canonical data. Contributors can attach an optional bounded rationale;
the rationale is retained with the proposal and visible to Data Oversight, but
does not become part of the canonical action replay.
## Increment plan
1. Product shell and anonymous local-draft builder.
2. Embedded Turso migrations, repositories, and seed data.
3. Credentials and Discord authentication.
4. Full equipment/stat builder driven by database definitions.
5. Publishing, discovery, ratings, comments, favorites, and copy flow.
6. Rich build guides and moderation.
7. Admin editors, validation, audit log, and data import/export.
# Administrator bootstrap
The authentication service promotes the first authenticated user to `admin` if
the database does not contain an administrator. Promotion uses one conditional
database update so concurrent first sign-ins cannot both become administrators.
Once an administrator exists, later sign-ins never alter roles.
# Database backup and restore
Embedded Turso databases contain contributor-entered source data and must be
preserved across development changes.
Create a timestamped development backup:
```sh
npm run db:backup
```
Create a production backup:
```sh
npm run db:backup:production
```
The scripts write versioned JSON documents beneath `data/backups/` by default.
Git ignores that directory because backups contain private user data, including
password hashes.
To restore, first create an empty database and apply the schema. Then run:
```sh
npm run db:restore -- data/backups/<backup-file>.json
```
The restore operation refuses to import into any database containing records.
This guard prevents accidental merging or overwriting of contributor data. Stop
the application before backup or restore so the embedded database file is not
locked by another process.
## Development database maintenance
Treat the development database as persistent data. Stop this project's
development server before backup, export, restore, migration, or other work
requiring exclusive access. Create and verify a JSON backup, perform the
prepared operation, verify schema state and critical record counts, then restart
and health-check the application. Prefer additive, repeatable migrations and
retain the verified backup if maintenance fails.
Production and staging remain shared persistent environments and retain the
deployment, backup, synchronization, and maintenance safeguards documented in
`docs/DEPLOYMENT.md`.
# ISAC audit and compensating reversals
Every successful ISAC mutation appends an event to `audit_log` through the
shared server audit utility. Events identify the operator, operational category,
specific action, target entity, timestamp, and useful before/after evidence. The
audit registry is read-only and requires `audit.view`; the seeded Auditor role
grants that permission without granting mutation capabilities.
Audit history is immutable. The server implements “Revert” as a new compensating
mutation rather than deletion or modification of the original event:
1. The operator must hold `audit.revert` in addition to any other access.
2. The reversion registry must support the original action and the event must
contain a usable before-snapshot.
3. The UI requires a second, explicit confirmation.
4. The server reloads the target and compares it with the original
after-snapshot, ignoring bookkeeping timestamps. A mismatch indicates newer
work and blocks automatic compensation.
5. The server revalidates referenced roles, permissions, and relationships
before restoration. Restoring administrator authority also requires full
administrative access.
6. The server restores the previous state and appends an `audit.reverted` event
whose target is the original audit-event ID.
7. The server rejects a second reversion of the same event.
# Moderated game-data contributions
ISAC data access uses `data.view`, `data.propose`, `data.manage`, and
`data.approve`. Data Viewers can inspect registries. Data Contributors can
submit the same editor forms, but the server serializes each request into
`data_change_proposals` instead of mutating canonical tables. Data Oversight
reviews the queue and must confirm approval or rejection.
Approval replays the stored form through its original SvelteKit server action.
This preserves canonical validation, relational updates, icon processing, and
the normal game-data audit event. Proposal creation and review decisions also
produce separate `data_moderation` audit events. Failed canonical validation
marks a proposal as failed without changing live data. Pending counts appear
alongside each data registry in ISAC so collaborators can see concurrent
suggestions.
The protected seeded roles are Data Viewer, Data Contributor, Data Editor, and
Data Oversight. Data Editor retains direct-write access for trusted operators;
Data Oversight combines direct-write access with queue review. New ISAC data
modules must use the shared moderation interception path and audit both the
final mutation and proposal lifecycle.
Mechanic programs remain part of their owning source record. Talent, OS
Protocol, Skill Chip, and Specialization proposals carry the complete typed
program with the rest of the source payload. Oversight reviews and approves that
program through the source module's canonical validation path. There is no
standalone Trigger proposal module or pending Trigger dependency.
Each authenticated, active registered agent receives `isac.access`, `data.view`,
and `data.propose` as baseline permissions in the authorization layer. The
authorization layer grants this baseline without a removable role assignment, so
existing users and each account-creation method receive the same access.
Registered agents can inspect live source data and submit moderated proposals,
but cannot mutate canonical records, review the queue, or access unrelated ISAC
operations without additional permissions. Unsupported destructive identity
operations, account deletion, incomplete snapshots, stale targets, and events
whose dependencies no longer exist are marked non-reversible or rejected.
Recovery for those cases requires a specialized workflow rather than inferred
database writes.
# System metrics
ISAC uses `/isac` as its canonical route namespace. The server redirects legacy
`/admin` URLs to their matching `/isac` location so old notifications and
external bookmarks remain usable.
The read-only `/isac/metrics` route requires `metrics.view` and collects a
point-in-time process-local health snapshot. Node and operating-system APIs
provide process CPU and memory, event-loop utilization and delay, host memory,
load average, core count, platform, and uptime. Embedded Turso pragmas provide
page allocation, page size, and free-page information; aggregate SQL queries
provide application record volumes.
The interface refreshes on demand and may poll every ten seconds while the
operator keeps auto-refresh enabled. CPU and event-loop values are deltas from
the previous server-side sample, so the first sample and samples separated by
long gaps provide direction rather than a monitoring-grade time series.
Production alerting and historical analysis should use a dedicated external
telemetry system. This surface omits secrets, environment-variable values, and
full database paths.
The same surface reports remove.bg paid credits and free API calls as separate
capacity pools. The scheduler reconciles the account balance no more often than
the configured interval, while successful image processing applies the
provider's `X-Credits-Charged` value to the persisted estimate. Warning and
critical transitions create one alert episode with a unique delivery for each
active Administrator. A later critical transition may alert again; repeated
scheduler runs within the same episode do not. Provider errors preserve the last
known balance and never expose the API key or raw response body.
# Weapon category bonuses
`weapon_category_attribute_bonuses` stores three ordered fixed Attribute
contributions for each Shotgun, Rifle, MMR, AR, LMG, and SMG category. Each
profile grants 5% Weapon Critical Hit Chance and 50% Weapon Critical Hit Damage.
Shotgun, Rifle, AR, LMG, and SMG grant 40% Headshot Damage; MMR grants 70%.
Weapon records do not own copies of these rows. Both builder catalog loaders
attach the matching category profile when they hydrate a weapon. Exotic weapons
retain an underlying category and inherit the same profile as a standard weapon
of that type. The combat summary preserves the existing equipped-weapon
semantics: each equipped weapon contributes its inherited profile to the Agent's
accumulated Attributes.
ISAC exposes one category-profile editor above the weapon registry. Operators
may change the three positive values, while the Attribute identities and order
remain fixed. Each successful save appends a
`game_data/weapon_bonus_profile.updated` audit event with the complete previous
and resulting profile.
# Referenced exotic weapon talents
Standard weapon talent pools remain reusable across weapons and use Talent 1 /
Top or Talent 2 / Bottom. Contributors create dedicated talents in the Talent
Registry under `Exotic Weapon`. The database stores these records as Weapon
talents with Exotic scope and no standard slot.
An exotic weapon references one active exotic talent through
`weapons.exotic_talent_id`. The weapon editor lists registered exotic talents
and identifies talents assigned to another weapon. Server validation rejects
standard talents, inactive talents, and talents already assigned elsewhere.
Changing or clearing a weapon relationship leaves the previous talent active and
available for another assignment.
Tier I and its modeled mechanic are required when a contributor saves an exotic
talent. Tier II may remain undocumented. An undocumented Tier II has no
`talent_tiers` row or conditional-effect value variant. A later Talent Registry
edit adds the Tier II projection and mechanic values without changing the weapon
reference.
# OS Protocol progression
OS Protocols use dedicated normalized tables because an assigned Talent has six
calculated states: its base value plus five upgrades. Each protocol owns one
`os_protocol_primary_effects` record and may own one `os_protocol_talents`
record. The optional Talent owns its mechanic program through the shared
conditional-effect tables. ISAC keeps the Talent workspace collapsed until an
operator assigns or opens it, and removing an existing Talent requires explicit
confirmation.
Dedicated profile tables normalize the three secondary attributes. One
`os_protocol_secondary_profiles` record exists for each configured quality +
attribute-type combination, with its three fixed stat/value pairs in
`os_protocol_secondary_effects`. Every matching protocol inherits that profile;
secondary values are not copied onto individual protocol records.
The unique primary value uses `base × (1 + (level − 1) / 49)` for levels 1–50,
while talent effects use `base × (1 + upgrades / 5)` for zero to five upgrades.
Server actions validate all relationships and append the full owned graph to the
existing `game_data` audit history.
### Weapon Mods and tiered weapon slots
`weapon_mod_slots` records each weapon's available slot vocabulary per tier. The
editor captures one shared set for T1/T2 and the single shared slot omitted at
T0, then expands that input into the normalized per-tier rows. Operators may
save partial rows when the in-game configuration is unavailable. ISAC derives
`CONFIG REQUIRED` until every available tier contains four unique slots at T0 or
five at T1/T2. Regular Weapon Mods declare a single slot type and one or more
compatible base weapon categories through `weapon_mod_compatible_categories`;
eligibility requires both a category match and the slot to exist on the equipped
weapon at its selected tier.
The eight code-defined slot types have stable UUIDs and one shared icon each.
Operators manage the artwork in the Weapon Mod registry through the game-data
icon pipeline. Each upload or removal appends a
`game_data/weapon_mod_slot_icon.updated` audit event, and contributors may send
the same action to Data Oversight. Public weapon cards use icon-only slot chips
when artwork exists, with the slot name available to screen readers and on
hover. Cards show the name when artwork is missing and retain the visible `T1+`
marker for slots unlocked at Tier 1.
Exotic Weapon Mods instead reference one `exotic_weapon_id`. They do not carry
regular category compatibility. The builder treats them as permanent equipment
and lets the user change only their level. They do not use quality profiles,
random primary rolls, secondary pools, or regular Mod Effects. Their canonical
contributions are fixed attribute references with explicit level-1 and level-20
values in `weapon_mod_exotic_attributes`; intermediate values use linear
interpolation.
All regular Weapon Mods permit Firepower, Toughness, or Engineering as a
builder-time primary choice. `weapon_mod_quality_values` stores each
definition's level-1 and level-20 primary roll endpoints for each quality;
primary values use linear interpolation from levels 1–20.
`weapon_mod_secondary_options` stores one global secondary Attribute pool for
all regular Weapon Mods. Each Attribute has Level 1 and Level 20 minimum and
maximum ranges for Specialized, Superior, and High-end quality through
`weapon_mod_secondary_quality_values`; values at intermediate levels use linear
interpolation. Secondary capacity is fixed by quality: Standard has zero,
Specialized has one, Superior has two, and High-end has two. A build selects
distinct Attributes from the pool up to that capacity. Individual Weapon Mod
records do not own secondary options or ranges. Pool updates append a
`game_data/weapon_mod_secondary_pool.updated` audit event. Mod Effects remain
triggerless, hold one or two Attribute references, and store fixed per-quality
values in `weapon_mod_effect_quality_values`.
### Gear Mods and tier slot capacity
Gear Mod definitions are standard-quality records only; there is no exotic Gear
Mod branch. Every definition supplies primary roll ranges for Standard,
Specialized, Superior, and High-end quality at Levels 1 and 20. Values between
those endpoints use linear interpolation. A build instance selects one primary
attribute from Firepower, Toughness, Engineering, Health, or Armor.
One global Gear Mod secondary Attribute pool serves every Gear Mod definition.
Each Attribute has Level 1 and Level 20 minimum and maximum ranges for
Specialized, Superior, and High-end quality through
`gear_mod_secondary_quality_values`; values at intermediate levels use linear
interpolation. Secondary capacity is fixed by quality: Standard has zero,
Specialized has one, Superior has two, and High-end has two. A build selects
distinct Attributes from the pool up to that capacity. Individual Gear Mod
records do not own secondary options or ranges. Gear tier still determines mod
slot capacity without per-item configuration: T0 gear has one Gear Mod slot; T1
and T2 gear have two.
The `gear_mods` and `gear_mod_quality_values` tables store individual definition
data. `gear_mod_secondary_options` and `gear_mod_secondary_quality_values` store
the family-wide pool. Pool updates append a
`game_data/gear_mod_secondary_pool.updated` audit event. Individual definition
mutations retain the `gear_mod.created`, `gear_mod.updated`,
`gear_mod.archived`, and `gear_mod.unarchived` audit actions. Contributor
submissions use the `gearmods` proposal module and remain non-canonical until
approved by Oversight.
# Skill Chip quality scaling
Each Skill Chip definition belongs to one specialization ability. Each set owns
one 2-piece bonus and may own one 3-piece Talent with a typed conditional rule.
ISAC keeps the complete Talent workspace, including its description and mechanic
program, inside one collapsible optional section.
All definitions use one fixed primary profile. The profile contains Skill
Duration, Skill Radius, Skill Cooldown Recovery, and Skill Intensity, with
direct minimum and maximum values for Standard, Specialized, Superior, and
High-end. Primary values apply to the assigned ability rather than the Agent.
Builder and shared Build pages label them `NOT YET DEFINED` and exclude them
from current Agent and combat totals.
All definitions also use one shared secondary Attribute pool. Each pool entry
stores direct minimum and maximum values for Specialized, Superior, and
High-end. Standard chips have no secondary slot, Specialized chips have one, and
Superior and High-end chips have two. A configured chip selects distinct
secondary Attributes up to its quality's capacity. Skill Chips have no item
level, so neither profile uses level interpolation.
Active definitions reference an active specialization ability through
`skill_chip_sets.specialization_ability_id`. An ability has capacity for three
active definitions. The save and restore actions enforce that capacity, and the
schema requires the foreign key for each row. A specialization focus is
builder-ready only when each of its three assigned abilities has all three
active Skill Chip definitions. Skill Chip mutations refresh readiness for
focuses that reference both the previous and resulting ability.
Every equipped quality counts toward the two-piece threshold. After the loadout
meets that threshold, only equipped High-end chips increase the interpolation
step. For `h` High-end chips, where `h` is zero through three, a configured
minimum and maximum resolve as `minimum + (maximum − minimum) × h / 3`. Zero
High-end chips resolve to the minimum, while three resolve to the maximum. The
editor stores endpoints and previews all four states.
ISAC presents the fixed primary profile and secondary pool as two shared
registry/editor cards above the definition registry. Successful profile saves
append `game_data/skill_chip_primary_profile.updated` and
`game_data/skill_chip_secondary_pool.updated`, respectively. Definition
mutations continue to capture their owned bonus, optional Talent, ability
relationship, and safe icon metadata in the `game_data` audit stream.
# Mechanic programs and loadout state
Mechanic programs belong to the record whose behavior they describe. One
`conditional_effect_rules` row owns the program for one standard or exotic
Talent, OS Protocol Talent, Skill Chip Talent, or Specialization tree rank. The
`source_type` and `source_id` pair identifies that owner. No standalone Trigger
record or shared Trigger relationship participates in execution.
The mechanic runtime accepts normalized timeline events for each equipped
mechanic. Current player pages do not construct this timeline or invoke the
runtime. `movement_snapshot.values.state` is `immobile`, `moving`, or
`sprinting`; the mechanic's moving and sprinting scenario controls select that
state, with sprinting taking precedence. Enabled Skill activation profiles emit
`skill_deployed` events on the same cadence as Skill calculations and identify
the deployed ability. An `on_event` Skill modifier applies to that activation
until the ability's next deployment or the encounter ends.
`skill_deployed.values.abilityId` and `skill_hit.values.abilityId` identify the
same ability for mechanics that bind an effect to the next deployed Skill.
Each source supplies a fixed value-variant contract:
- Standard Talents require Tier I and Tier II values. Exotic Talents require
Tier I and may omit Tier II until contributors document its game values.
Runtime calculations use Tier I while an equipped exotic Talent has no Tier II
mechanic variant.
- OS Protocol Talents store a base value. An outcome may use an expression that
reads `equipment.protocolTalentUpgrades`, from zero through five, so selected
values can scale to twice the base at upgrade five.
- Skill Chip Talents store minimum and maximum values for quality composition
interpolation.
- Specialization tree-rank effects store the rank's base value.
The rule row stores the script version, source-defined scenario-input controls,
configuration state, and one of three simulation-readiness states:
- `ready`: the runtime has enough typed data to calculate the mechanic.
- `requires_assumptions`: the mechanic needs one or more scenario inputs before
the runtime can include it.
- `captured_only`: ISAC preserves the authored mechanic, but the runtime cannot
calculate it.
The code-defined `MECHANIC_EVENT_REGISTRY` is the event authority. Its 28
normalized events are `shot_fired`, `weapon_hit`, `skill_deployed`,
`signature_ability_deployed`, `skill_hit`, `skill_multishot_triggered`,
`agent_hit`, `health_damage_taken`, `damage_dealt`, `enemy_killed`, `assist`,
`ally_downed`, `agent_downed`, `ally_rescued`, `grenade_thrown`,
`medical_kit_used`, `healing_applied`, `extra_health_granted`,
`extra_health_changed`, `movement_snapshot`, `proximity_snapshot`,
`firing_started`, `firing_stopped`, `reload_started`, `reload_completed`,
`weapon_swapped`, and `tick`. Event details such as target, distance, critical
state, source, movement, and nearby counts travel as typed event values. The
event registry publishes authoritative field metadata where branches need a
bounded contract. For those events, the mechanic editor derives its event-field
controls from that metadata. For example, `weapon_hit.values.headshot` is a
boolean that distinguishes headshots from body shots. `enemy_killed` and
`assist` may carry `values.assignedAbilityActive`; a Skill Chip Talent uses that
boolean to restrict a kill or assist trigger to its assigned ability's active
period. `skill_deployed` and `signature_ability_deployed` may carry
`values.assignedAbilityDurationSeconds` to start assigned-ability state. The
runtime expires that state before it evaluates another event at the same
timestamp.
`weapon_hit.values.aiming` records whether the Agent was aiming down sights when
the hit resolved. A producer omits the field when it lacks authoritative aiming
state; the runtime does not replace missing evidence with a scenario assumption.
Predicates compare this field explicitly with `true` or `false`; negation and
inequality cannot turn missing evidence into a match. A `weapon_hit` or
`skill_hit` event may include an event-target-centered area sample. The bounded
sample lists up to 256 valid ally and enemy entity IDs with their distances from
the hit target and includes that target as an enemy at zero meters. Duplicate
IDs, negative distances, or a missing center target invalidate the evaluation.
`extra_health_changed` is Agent-scoped. Normalized timeline producers emit it
when the Agent receives an Extra Health grant, when the remaining amount
changes, and when it is depleted to zero. Every event requires the boolean
`values.hasExtraHealth`. An event may include the non-negative
`values.currentExtraHealth`; when present, the amount must agree with the
boolean status. `extra_health_granted` represents a grant action and remains a
separate event from changes to the Agent's current Extra Health state.
The optional Action Family catalog provides authoring recipes for common
mechanics. Applying a recipe copies its typed scenario inputs, state pools, and
clauses into the source-owned program. Those branches become editable
authoritative data; the recipe key remains provenance, not a runtime dependency.
Adding another recipe appends branches. Replacing a program with a recipe
requires confirmation because replacement discards the current branch graph.
A Talent may save an empty `captured_only` program while an author records its
name, category, image, and source description. The registry marks that Talent
`CONFIG REQUIRED`, and calculations exclude it. Changing readiness to `ready` or
`requires_assumptions` restores strict branch, numbered-outcome, and
description-placeholder validation.
The program graph uses three child tables:
- `conditional_effect_state_pools` defines agent- or target-scoped flags,
counts, stacks, durations, scalar values, marks, and resources. Each pool owns
its bounds, expiry policy, refresh policy, and best-case value.
- `conditional_effect_clauses` listens for one event from the registry. A clause
owns a typed predicate, reducer, optional cooldown, optional weight
expression, state mutations, and optional recipe provenance. Reducers convert
event streams into counts, distinct or consecutive matches, sliding windows,
durations, intervals, or derived values. Mutations can set, add, consume,
clear, derive, or transfer state. A mutation may bind a target-scoped pool to
the current event target, event actor, or another typed entity selector. Each
target owns an independent value and expiry timer. A clause may require active
assigned-ability state and carry `procChancePercent`. The runtime evaluates
that chance after predicates and cooldown checks, before reducers, mutations,
and outcomes.
- `conditional_effect_outcomes` applies Attribute modifiers, direct damage,
restoration, resource costs, or markers. Each outcome names its target scope,
value basis, optional resource or Attribute, combat timing, optional state
band, and phase: `on_event`, `after_event`, `on_enter`, `on_exit`, or
`while_active`. One typed value or expression exists for every variant
required by the owning source. The common value models are static values, live
Attribute values, percentage-normalized Attribute values, event-field values,
reducer output, source-defined scenario values, and OS Protocol upgrade
scaling. An outcome may carry `probabilityPercent`; each eligible outcome
rolls separately. Restoration outcomes may deliver their total immediately or
continuously over an explicit duration.
Target-scoped direct damage may use `event_target_area` targeting for an
explosion around its event target. This targeting selects valid enemies and
stores a positive radius in meters. It is valid only for `on_event` or
`after_event` outcomes. The legacy `within_radius` shape remains descriptive
targeting metadata and does not select runtime entities.
The generic runtime in `src/lib/calculations/mechanic.ts` processes combat
events in timestamp order. Predicates decide whether an event enters a clause.
Reducers convert event streams into activations or derived values. The runtime
applies state mutations, enforces pool bounds and expiry, emits outcomes in
phase order, and records telemetry for each activation.
When `event_target_area` damage activates, the runtime requires the triggering
event's authoritative area sample. It includes valid enemies whose distance is
less than or equal to the configured radius, sorts them by entity ID, and emits
one equal damage outcome for each entity. The outcome chance resolves once
before fan-out. A missing sample invalidates the evaluation instead of producing
single-target damage or treating the area as empty.
Custom evaluation uses a Mulberry32 stream seeded by `randomSeed`; an omitted
seed resolves from the stable mechanic program ID. The returned evaluation
reports the seed. Maximum mode resolves every nonzero clause or outcome chance
as successful. `extend_duration` changes only an active assigned ability's
remaining duration. `start_cooldown_on_max` writes an expiring Agent cooldown
pool only when an earlier mutation in the same clause transitions a capped stack
pool to its maximum.
Each write to an expiring pool refreshes its timer. When a pool expires,
`on_exit` outcomes evaluate stack thresholds and per-stack expressions against
the final value before reset. The runtime records the bound target on emitted
outcomes. A one-second target debuff with a one-second damage cadence can
therefore resolve once at its refreshed shared expiry without inventing
intermediate ticks.
Live Attribute references use a stable Attribute ID and read the authoritative
loadout total that the builder captures for the evaluation pass. Percentage
Attributes retain their displayed units, so an expression that applies a
coefficient to a percentage divides the Attribute value by 100. Evaluation fails
when the loadout does not supply a referenced Attribute; the runtime does not
substitute zero.
The read-only `combat.currentHealthPercent` reference divides resolved current
Health by resolved maximum Health and returns a value from 0 through 100. The
runtime derives it when a predicate is evaluated instead of storing a second
Health value that could become stale. Pre-event timing reads the combat snapshot
before mechanic branches process the event. Current timing observes
deterministic resource changes from earlier branches at the same event.
Health-band mechanics use strict upper bounds and inclusive lower bounds, so
bands do not overlap and an exact threshold enters the next weaker band.
Maximum-potential evaluation applies the same predicates before emitting passive
outcomes.
Continuous restoration resolves the outcome's total when the branch activates,
then delivers that total at a constant rate over the configured duration.
Activations create independent streams, overlapping streams add, and restoration
above the resource maximum is lost when it occurs. The runtime integrates
delivery at event and expiry boundaries before processing the event at that
timestamp, which keeps results independent of an arbitrary tick interval.
Periodic behavior requires an interval in the reducer. The runtime does not
infer a one-second cadence from missing data. A mechanic with an unknown cadence
uses `requires_assumptions`, identifies the missing input, and stays out of the
calculation until an operator or builder scenario supplies it.
The runtime derives a read-only pseudo-language listing from the typed program.
ISAC uses that listing to explain events, predicates, reducers, state changes,
phases, and formulas. Planned player calculations can reuse it when those pages
consume mechanic outcomes. No route accepts the generated text as input, and no
server process executes it. Operators edit structured fields; the formatter
regenerates the listing after each change.
## AI-assisted mechanic drafting
Manual mechanic authoring remains the complete authoring path. The AI assistant
produces an optional draft for the existing structured editor. It cannot save,
publish, approve, or submit game data, and it cannot alter application code. An
author may ignore, revise, or replace any generated field through the manual
controls.
The shared mechanic editor exposes the mechanic assistant for standard and
exotic Talents, OS Protocol Talents, Skill Chip Talents, and Specialization
tree-rank effects. Calculation revisions use a separate assistant contract
because their typed stage schema and publication authority differ from
source-owned mechanic programs.
The server owns a versioned mechanic-authoring prompt and context builder. Each
request receives the source type, progression contract, submitted game text,
current draft when supplied, code-defined mechanic registries, and the active
ISAC Attribute catalog. Attribute context includes stable IDs, names, units,
descriptions, and calculation metadata. The client cannot replace the system
instructions, choose arbitrary models, attach files, add tools, or add external
URLs.
The deterministic draft parser owns the canonical outcome-capability manifest.
The AI context serializes that manifest instead of maintaining a second outcome
grammar. The parser rejects outcome fields absent from the manifest. Contract
tests build a draft from each advertised capability example and pass it through
the parser, so CI blocks a prompt that advertises unsupported fields. A mechanic
grammar change must update the manifest and example, pass the round-trip test,
and increment the prompt contract version before release.
The OpenAI integration uses the Responses API with Structured Outputs, no tools,
and `store: false`. The application stores the bounded clarification history
needed for the current drafting session. Requests use a fixed server-configured
model, a privacy-preserving user safety identifier, input and output limits,
timeouts, duplicate-request protection, and per-agent rate limits. The API key
remains on the server.
The model returns one structured result:
- `clarification_required` contains bounded questions whose answers affect the
calculation model.
- `draft_ready` contains the typed mechanic draft, assumptions, warnings,
readiness state, and a short explanation.
- `capability_gap` names unsupported runtime behavior, the nearest supported
representation, an optional workaround, and the schema, runtime, editor,
reference, and test changes a developer would need to add support.
For a mechanic capability gap, the client builds a developer prompt from the
exact analyzed wording, clarification transcript, source profile, and structured
gap response. The author can copy that prompt without another model request. The
author can also submit the gap to active full administrators. The submission
endpoint validates bounded fields, confirms that the request ID belongs to the
author's audited `capability_gap` generation, suppresses repeat submissions,
sends mandatory ISAC notifications, and appends a `mechanic_gap.submitted` event
to the `ai_assistance` audit category. The notification links to the filtered
audit evidence. Submission does not mutate the mechanic draft or parent
game-data record.
The server validates `draft_ready` output against the response schema and the
same deterministic mechanic parser used by manual authoring. Unknown events,
Attributes, value models, scopes, operators, reducers, or outcomes fail
validation. The client receives no draft when local validation fails.
The author reviews a validated draft and chooses whether to apply it to the
unsaved editor. Applying a draft changes client form state only. The existing
module action performs canonical validation, contribution interception,
approval, persistence, and game-data audit logging after the author initiates
the normal save flow.
The `ai.mechanic.generate` permission authorizes generation and clarification
requests. The server checks it for every request. Agents without the permission
may see the disabled assistant entry point, but they retain all manual controls
allowed by their data role.
Successful generation operations append sanitized events under the
`ai_assistance` audit category. Events identify the operator, source type,
prompt-context version, model, result type, token usage, request ID, and
validation result. Audit snapshots exclude the API key, environment
configuration, hidden model reasoning, authentication data, and unbounded prompt
content. Applying a draft changes client state and creates no server event. The
owning module's game-data audit event remains the record of any saved mechanic
change.
### AI-assisted calculation-revision drafting
The calculation assistant converts an operator's plain-language combat-model
request into an optional draft revision. It receives the current active or
edited revision, the calculation-stage vocabulary, weapon and Skill profile
contracts, and the active ISAC Attribute catalog. Its versioned prompt,
structured response schema, validation, and application adapter remain separate
from mechanic drafting.
The assistant returns clarification questions, a validated revision draft, or a
calculation capability gap. A capability gap identifies the missing stage,
operator, curve, binding, runtime behavior, or validation rule and lists the
application and test changes needed to support it. The server rejects unknown
stage keys, locked-position changes, duplicate bindings, invalid Attribute
references, unsupported operators, and malformed curve or cap values through the
deterministic calculation-revision parser.
The `ai.calculation.generate` permission authorizes calculation drafting and
clarification requests. It does not grant revision mutation or publication.
Applying a result changes the open revision form in the client. The operator
must review and save that draft through the existing revision action. The
separate `calculation.publish` permission and activation confirmation remain
mandatory before the saved draft becomes the active authored model. Current
player pages do not consume that model.
Successful calculation-generation requests append sanitized events under
`ai_assistance`. These events use calculation-specific actions, identify the
base revision and generated contract version, and follow the same request,
token-usage, result, and secret-exclusion rules as mechanic-assistance events.
Applying a draft creates no server event. Existing `calculation_configuration`
events remain the evidence for revision saves and activation.
Source authors define planned player scenario controls as typed toggle, slider,
or numeric inputs. Each input records its label, guidance, bounds when
applicable, default value, and best-case value. ISAC retains these definitions
for continued mechanic authoring. Current Builder and shared Build pages do not
render or evaluate them. Their calculation panels display `COMING SOON` instead.
Loadout schema version 6 stores one focus, selected tree ranks, nine Skill
Chips, independent T1/T2 choices for each equipped gear and weapon Talent, a
unified conditional-effect state map, encounter assumptions, per-ability
activation controls, and `calculationMode`. Runtime readers accept the exact
version 6 structure. A backed-up migration translated version 5 equipment
Talents from their previous equipment-tier-derived behavior. The schema retains
its simulation fields, but the current player UI ignores them. Version 4 remains
obsolete prototype data.
## Planned player calculation modes
Loadout v6 defines `maximum` and `custom` modes for the planned simulation UI:
- Maximum mode supplies each scenario input's best-case value, selects peak
conditional-effect state, and runs selected abilities on cooldown against
their configured target caps. The interface hides granular assumptions in this
mode.
- Custom mode uses the agent's encounter inputs, effect states, activation
rates, target counts, and other supported assumptions. A missing required
input produces `REQUIRES_ASSUMPTIONS` rather than a fabricated value.
Saved and shared loadouts preserve the selected mode as compatibility data. The
current Builder and shared Build UI does not expose a mode selector, read
encounter assumptions, or execute either mode. Explicit `COMING SOON` panels
reserve the player-facing space for these calculations.
## Specialization ability attacks
`specialization_ability_attacks` stores intrinsic base damage, Blast, Pierce, or
Shredding damage, direct or periodic delivery, hit or tick counts, cast time,
target cap, and charge behavior. `skill_attribute_calculation_profiles` binds
Attributes to the versioned Skill damage and cadence stages. Cooldown Recovery
changes charge and activation readiness. Duration changes the number of periodic
ticks.
Periodic delivery waits one configured tick interval before its first tick and
excludes ticks at or after the encounter boundary. The Skill engine reevaluates
active modifier windows at each delivered hit or tick. A missing tick interval
marks the attack as requiring configuration; the engine does not supply a
default cadence.
# Versioned combat calculations
`calculation_revisions` supplies one active rule set. Operators can change draft
revisions, while active and retired revisions stay immutable.
`calculation_stages` stores the ordered typed pipeline. The base-damage stage
holds the first position, and the firing-cycle stage holds the last.
`attribute_calculation_profiles` maps an Attribute to a weapon stage with its
weight, optional caps, curve, and weapon-category scope.
ISAC keeps combat revision editing, publication, calibration, assisted
suggestions, and the calculation AI assistant available so operators can
continue authoring the model. Builder and shared Build pages do not load the
active revision or consume revision, calibration, assistant, mechanic-runtime,
or combat-engine output at the current player-facing boundary.
The shared combat engine consumes weapon facts, accumulated Attributes,
calculation profiles, and mechanic outcomes. It produces four hit values, burst
DPS, reload-aware sustained DPS, encounter DPS, and stack telemetry. Custom mode
walks the expected shot timeline from RPM, magazine size, reload time, hit rate,
configured burst size, pauses between bursts, and encounter duration. It
resolves the active Attribute state at each landed shot before calculating that
shot's expected damage.
The deterministic equipment path scales a weapon definition's level-1 damage
range to the equipped item level, then the agent selects one damage value inside
that valid range. A combat invocation receives `equipped_weapon.damage` as its
base damage and consumes that number once. The combat pipeline has no
`level_scaled_damage` value and must not apply item-level scaling a second time.
The generated calculation listing names each typed pipeline stage and its bound
Attributes. The listing explains the active revision with statements such as
`TAKE equipped_weapon.damage AS base_damage`, `ADD`, `APPLY`, `WEIGHT`, and
`RETURN`. The typed stages and profiles remain authoritative. The application
neither parses nor executes the listing.
The Headshot Kill Share assumption marks the matching portion of generated kill
events for headshot-kill triggers. Rate of Fire, Magazine Size, and Reload Speed
bindings change the firing-cycle calculation. Maximum mode applies maximum
state. Custom mode can set an effect to Peak, Encounter, or Off.
The Skill engine uses a dedicated intrinsic ability-damage model. It supports
direct and periodic delivery, critical and multi-shot expectation, target caps,
per-ability activation rates, and charge recovery. Skills do not headshot.
Dynamic Talent and tree-effect windows apply to each Skill hit at its delivery
time. Encounter-triggered Skill modifiers use the primary weapon timeline until
the builder supports a combined weapon-rotation model.
The engine returns `INSUFFICIENT_DATA` with missing requirement keys when a
calculation lacks required inputs or an Attribute binding. Callers must render
that state instead of a partial result. Current player routes do not call the
engine; their planned calculation surfaces display `COMING SOON`.
`calculation_calibration_cases` stores a versioned loadout snapshot, observed
hit values, timing values, optional encounter DPS, and the model prediction.
Ready cases require the four hit values plus magazine and reload times. The
assisted suggestion action compares ready observations with their predictions
and creates a draft revision for operator review.
Rule activation requires `calculation.publish` and a confirmation. ISAC records
draft creation, draft edits, publication, calibration changes, and assisted
suggestions under the `calculation_configuration` audit category. ISAC data
export and environment synchronization include calculation rules, typed mechanic
programs, ability attacks, calibration cases, and their relationships in
dependency order.
## OS Protocol upgrade expressions and health timing
OS Protocol talents expose zero through five upgrade states. Value expressions
reference `equipment.protocolTalentUpgrades` and use the linear multiplier
`1 + upgrades / 5`. Base values resolve at upgrade zero, and scalable values
reach twice their base at upgrade five. Each outcome chooses whether to use this
multiplier, so a fixed resource cost can remain unchanged while restoration and
damage increase.
Happy Survival uses three clauses:
1. Its proximity clause records the current enemy count within 10 meters.
2. Its periodic clause restores
`min(enemyCount × 0.5%, 2.5%) × (1 + upgrades / 5)` of maximum Health. The
reducer reads the configured interval from an explicit scenario input. The
runtime caps applied restoration at missing Health.
3. Its `shot_fired` clause consumes 2% of maximum Health, then deals direct
damage equal to `0.6% × (1 + upgrades / 5)` of current Health. The
`resource_cost` outcome runs first. The damage outcome uses the remaining
current Health after that cost, rather than the pre-event Health value.
At upgrade five, the restoration becomes 1% of maximum Health per nearby enemy
up to 5%, and the shot damage becomes 1.2% of current Health. The 2% Health cost
does not scale.
# Game-data icon storage
Talents, brand sets, OS Protocols, Skill Chips, Specializations, and shared
Weapon Mod slots reference uploaded artwork through the polymorphic
`game_data_icons` metadata table. The shared server pipeline accepts transparent
SVG, PNG, WebP, and AVIF sources, rejects active SVG content and external
references, and uses Sharp’s native worker pool to rasterize and normalize each
accepted icon to a transparent 256 × 256 WebP.
Normalized files live beneath the ignored `data/game-icons/` directory. The
validated `/icons/game-data/[type]/[id]` endpoint serves them with immutable
cache headers and disables MIME sniffing. ISAC save actions process files before
mutation, apply upload or removal after record validation, and include safe icon
metadata, rather than image bytes, in audit snapshots.
Icon metadata is optional. Canonical records and contributor proposals may be
created without an upload, which lets contributors submit values and artwork in
separate dependency-safe changes. The shared `MISSING_ITEM_ICON_URL` constant
and `ItemIcon` component render `/missing-item-icon.svg` for missing artwork.
The placeholder does not enter `game_data_icons`. A later edit or proposal can
attach the real icon without recreating the canonical item.
`source_entity_type` and `source_entity_id` link each `game_data_icons` row to a
`managed_images` record. Both records share one storage key and one WebP file.
The managed record adds the searchable library name, category, tags, creator,
dimensions, and assignment state. Game-data icon saves upsert this record, keep
operator-edited metadata, and merge current entity-context tags. Removing a
source icon removes the linked library record and shared file.
The additive database migration registers older game-data icons and selects an
administrator or the earliest active account as the migration actor. ISAC and
managed-image exports accept linked storage keys. Environment synchronization
keeps the library metadata and one copy of each file.
# Oversight notification dispatch
The notification router sends Data Change review items to agents who hold
`data.approve` and Avatar Oversight review items to agents who hold
`avatars.review`. Each reviewer chooses Immediate, 15-minute, one-hour, or daily
delivery for each authorized queue. Submission creates Immediate alerts;
`oversight_notification_queue` stores consolidated alerts.
The production Compose scheduler calls the internal scheduler endpoint once per
minute. That endpoint runs the idempotent dispatcher in the application process,
which owns the embedded-database connection. Do not run a second host cron job.
The dispatcher groups due rows by reviewer, queue, cadence, and delivery window.
Daily rows use the next 10:00 AM wall-clock boundary in `America/New_York`,
including daylight-saving transitions. Dispatched rows retain `delivered_at`
evidence, and later queries exclude them. Existing ISAC notification delivery
preferences determine optional browser and email fan-out after the dispatcher
creates the consolidated inbox message.
## Automated ISAC system messages
Automated message definitions live in `automated_system_messages` and are
managed through ISAC → System Configuration → Automated Messages by agents with
`system.notifications.manage`. The audit log records edits under the
`system_configuration` audit category. Definitions identify a lifecycle event,
enabled state, delay in minutes, message content, primary destination, and
structured action callouts.
Password, magic-link, and Discord registration emit `account.created`. The
delivery table has a unique message/user/event/event-ID constraint, so the same
account creation cannot deliver the Welcome Agent message more than once. The
default welcome introduces News, Database, Builds, Guides, Builder, ISAC
Contributor Mode, and the SHD Discord community.
The registration request delivers Immediate definitions.
`automated_system_message_deliveries` stores definitions with a delay. The same
Compose scheduler tick runs the delayed dispatcher. Only one scheduler instance
should call the embedded-database application. Delivered rows retain both their
delivery timestamp and generated notification ID.