feat: add source-owned combat mechanic programs

This commit is contained in:
JB
2026-07-27 13:35:00 -04:00
parent 5a67b252aa
commit 4eb754e7c8
62 changed files with 14815 additions and 905 deletions
+278 -119
View File
@@ -1,8 +1,8 @@
# SHD.build architecture
The project is built in vertical slices.
The team builds the project in vertical slices.
## Planned stack
## Application stack
- SvelteKit 2 / Svelte 5 with TypeScript
- Embedded Turso Database via `@tursodatabase/database` for durable application data
@@ -18,14 +18,13 @@ The project is built in vertical slices.
- Editor: manage builder source data.
- Admin: manage users, roles, source data, and audit history.
Server code checks authorization for each mutation. Hiding an admin link does
not grant authorization.
Server code checks authorization for each mutation. UI visibility has no effect
on server authorization.
Weapon and gear range calculations must preserve unrounded decimal endpoints
and follow the empirically validated progression rules in
[`STAT_LEVEL_SCALING.md`](STAT_LEVEL_SCALING.md). In particular, leveling is
linear rather than compounded and game-matching integer values are truncated
only at the presentation boundary.
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
@@ -40,7 +39,7 @@ 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
persistent volume is attached.
deployment attaches a persistent volume.
## Site launch controls
@@ -94,7 +93,7 @@ closed.
7. Admin editors, validation, audit log, and data import/export.
# Administrator bootstrap
The first successfully authenticated user is promoted to `admin` if the
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.
@@ -117,7 +116,7 @@ npm run db:backup:production
```
The scripts write versioned JSON documents beneath `data/backups/` by default.
That directory is ignored by Git because backups contain private user data,
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:
@@ -152,30 +151,30 @@ 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. “Revert” is implemented as a new compensating
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 independently hold `audit.revert`.
2. The original action must be explicitly supported and contain a usable
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. Referenced roles, permissions, and relationships are revalidated before
restoration. Restoring administrator authority additionally requires full
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. A reverted event cannot be reverted again.
7. The server rejects a second reversion of the same event.
# Moderated game-data contributions
ISAC data access is split into `data.view`, `data.propose`, `data.manage`, and
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 request is serialized into `data_change_proposals`
instead of mutating canonical tables. Data Oversight reviews the queue and must
explicitly confirm approval or rejection.
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
@@ -190,12 +189,18 @@ 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.
Every authenticated, active registered agent receives `isac.access`,
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. This baseline is intentionally implicit rather than a removable role
assignment, so it applies consistently to existing users and every account
creation method. Registered agents can inspect live source data and submit
moderated proposals, but cannot directly mutate canonical records, review the
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
@@ -204,7 +209,7 @@ specialized workflow rather than inferred database writes.
# System metrics
ISAC uses `/isac` as its canonical route namespace. The server permanently
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.
@@ -217,10 +222,10 @@ 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 or infrequently requested sample
is directional rather than a monitoring-grade time series. Production alerting
and historical analysis should eventually use a dedicated external telemetry
system. This surface deliberately omits secrets, environment-variable values,
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
@@ -234,27 +239,27 @@ last known balance and never expose the API key or raw response body.
# Weapon-owned exotic talents
Standard weapon talent pools remain globally reusable and classified by Talent
1 / Top or Talent 2 / Bottom. An exotic talent is different: one exotic weapon
owns one dedicated talent, referenced by `weapons.exotic_talent_id`.
Standard weapon talent pools remain reusable across weapons and use Talent 1 /
Top or Talent 2 / Bottom. One exotic weapon owns each exotic talent through
`weapons.exotic_talent_id`.
The weapon editor creates and updates the underlying normalized `talents`,
`talent_tiers`, trigger links, effect links, description template, and Exotic
weapon scope together with the weapon. Server validation rejects an exotic
talent ID not already owned by the edited weapon and rejects any talent shared
by another weapon. Converting a weapon back to standard clears the relationship
and archives its now-unowned exotic talent. Exotic talent creation and editing
are rejected by the shared Talent Registry so ownership cannot be bypassed.
`talent_tiers`, description template, and typed conditional rule together with
the weapon. Server validation rejects an exotic talent ID owned by another
weapon and rejects any shared Talent. Converting a weapon back to standard
clears the relationship and archives its now-unowned exotic Talent. The shared
Talent Registry rejects exotic Talent creation and editing so contributors
cannot bypass weapon ownership.
# OS Protocol progression
OS Protocols use dedicated normalized tables because their talent progression
has six calculated states (base plus five upgrades) rather than the shared
two-tier talent model. Each protocol owns one `os_protocol_primary_effects`
record and one `os_protocol_talents` record. Talent effects and reusable
triggers live in their respective join tables.
record and one `os_protocol_talents` record. Each protocol Talent owns its
mechanic program through the shared conditional-effect tables.
The three secondary attributes are normalized separately. One
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;
@@ -275,16 +280,17 @@ single slot type and one or more compatible base weapon categories through
and the slot to exist on the equipped weapon at its selected tier.
Exotic Weapon Mods instead reference one `exotic_weapon_id`. They do not carry
regular category compatibility, are considered permanently equipped, and may
only be leveled. 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 interpolate linearly.
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 the shared
level-1 and level-20 primary roll endpoints for each quality; primary values
interpolate linearly from levels 1–20.
use linear interpolation from levels 1–20.
`weapon_mod_secondary_options` is the shared pool from which a build selects
two distinct secondary attributes. Every option owns a minimum and maximum at
@@ -298,7 +304,7 @@ values in `weapon_mod_effect_quality_values`.
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 scale linearly. A build instance selects one primary attribute
those endpoints use linear interpolation. A build instance selects one primary attribute
from Firepower, Toughness, Engineering, Health, or Armor.
The two secondary slots draw distinct attributes from one shared option pool.
@@ -306,8 +312,8 @@ Each available secondary attribute stores its own minimum and maximum for all
four qualities. Gear tier determines capacity without per-item configuration:
T0 gear has one Gear Mod slot; T1 and T2 gear have two.
Canonical data is stored in `gear_mods`, `gear_mod_quality_values`,
`gear_mod_secondary_options`, and `gear_mod_secondary_quality_values`.
The `gear_mods`, `gear_mod_quality_values`, `gear_mod_secondary_options`, and
`gear_mod_secondary_quality_values` tables store canonical data.
Successful mutations use `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
@@ -317,16 +323,16 @@ approved by Oversight.
Skill Chip sets support Superior and High-end chip instances. Each set owns one
2-piece bonus, distinct primary and secondary stat-option pools, and one
3-piece talent with reusable trigger links and normalized effect rows. Primary
and secondary options reference active attributes that define Skill Chip
minimum and maximum roll values; a configured chip rolls one primary and two
distinct secondary stats from those separate pools.
3-piece Talent with a typed conditional rule. Primary and secondary options
reference active Attributes that define Skill Chip minimum and maximum roll
values. A configured chip rolls one primary and two distinct secondary stats
from those pools.
Every active Skill Chip definition belongs exclusively to one active
specialization ability through `skill_chip_sets.specialization_ability_id`.
An ability has capacity for exactly three active definitions. The save and
restore actions enforce that capacity, and the foreign key is required for
every row. A specialization focus is builder-ready only when each of its three
Each active Skill Chip definition belongs to one 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.
@@ -337,48 +343,176 @@ three, a configured minimum and maximum resolve as
`minimum + (maximum − minimum) × h / 3`. Zero High-end chips therefore produce
the minimum, while three produce the maximum. The editor stores endpoints and
previews all four states, preventing duplicated tier data from diverging.
Skill Chip mutations, owned relationships, and safe icon metadata are captured
in the `game_data` audit stream.
The `game_data` audit stream captures Skill Chip mutations, owned relationships,
and safe icon metadata.
# Specialization combat effects and loadout state
# Mechanic programs and loadout state
`specialization_combat_effects` stores reusable effect identity and description
within one Specialization. A node rank attaches that identity through
`specialization_tree_rank_combat_effects` and supplies the trigger count,
trigger window, duration, stack limit, and stack gain. The first supported
lifecycle uses an enemy-hit counter that resets after a proc. Each stack gain
refreshes one shared duration, and expiry clears all stacks.
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.
`specialization_tree_rank_combat_modifiers` maps each attachment to one or more
Attributes with a value per stack. This split lets ranks share effect language
while changing their trigger and values. Specialization mutations capture the
attachment and modifier sets in the `game_data` audit event. Contributors can
submit effect definitions and node attachments through the existing
Specialization proposal workflow.
Each source supplies a fixed value-variant contract:
Loadout schema version 2 stores one focus, the selected rank for each tree node,
nine Skill Chips, conditional-effect modes, and encounter assumptions. Each of
the three focus abilities owns three chip positions. Runtime readers accept
only the exact version 2 structure. Local drafts, saved builds, shared builds,
and calibration snapshots reject older or partial structures instead of
repairing them. Database migration removes obsolete snapshots after the
required environment backup.
- Standard and exotic Talents require Tier I and Tier II values.
- 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 19
normalized events are `shot_fired`, `weapon_hit`, `skill_deployed`,
`skill_hit`, `agent_hit`, `health_damage_taken`, `damage_dealt`,
`enemy_killed`, `assist`, `healing_applied`, `extra_health_granted`,
`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 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.
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 of the 19 events. 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.
- `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,
event-field values, reducer output, source-defined scenario values, and OS
Protocol upgrade scaling.
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.
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 and the builder use that listing to explain events, predicates, reducers,
state changes, phases, and formulas. 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.
Source authors define the builder controls required by their mechanic as typed
toggle, slider, or numeric scenario inputs. Each input records its label,
guidance, bounds when applicable, default value, and best-case value. These
controls appear only in advanced scenario configuration. Maximum mode supplies
best-case values. Custom mode uses the values saved with the loadout. A
mechanic with an undefined required assumption remains excluded and reports
`REQUIRES_ASSUMPTIONS`.
Loadout schema version 4 stores one focus, selected tree ranks, nine Skill
Chips, a unified conditional-effect state map, encounter assumptions,
per-ability activation controls, and `calculationMode`. Runtime readers accept
the exact version 4 structure. A backed-up prototype migration removes drafts,
saved builds, and calibration snapshots that use incompatible payloads.
## Calculation modes
The builder offers `maximum` and `custom` modes.
- 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. Calculation results label
the mode so readers can distinguish a best-case ceiling from a scenario-based
estimate.
## 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. Draft revisions can
change, while active and retired revisions stay immutable.
`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 stage with its weight,
optional caps, curve, and weapon-category scope.
`attribute_calculation_profiles` maps an Attribute to a weapon stage with its
weight, optional caps, curve, and weapon-category scope.
The shared combat engine consumes weapon facts, accumulated Attributes,
calculation profiles, and conditional effects. It produces four hit values,
burst DPS, reload-aware sustained DPS, encounter DPS, and stack telemetry.
Encounter mode walks the expected shot timeline from RPM, magazine size,
reload time, hit rate, firing activity, and encounter duration. Peak mode
applies maximum stacks, and Off mode excludes the effect.
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 builder 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.
Combat 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
@@ -389,35 +523,60 @@ active revision for each request.
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 an operator to review.
and creates a draft revision for operator review.
Rule activation requires `calculation.publish` and an explicit confirmation.
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,
calibration cases, and Specialization combat-effect tables in dependency order.
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, and Skill Chips 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
or externally referenced SVG content, and uses Sharp’s native worker pool to
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 and are
served by the validated `/icons/game-data/[type]/[id]` endpoint with immutable
cache headers and MIME sniffing disabled. ISAC save actions process files
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, allowing values and artwork to be contributed in
separate dependency-safe changes. Missing artwork is represented at render
time by the static `/missing-item-icon.svg` asset through the shared
`MISSING_ITEM_ICON_URL` constant and `ItemIcon` component. The placeholder does
not enter `game_data_icons`. A later edit or proposal can attach the real icon
without recreating the canonical item.
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.
@@ -433,14 +592,14 @@ keeps the library metadata and one copy of each file.
# Oversight notification dispatch
New Data Change and Avatar Oversight review items are routed only to agents who
currently hold `data.approve` or `avatars.review` respectively. Each reviewer
chooses Immediate, 15-minute, one-hour, or daily delivery independently for
each authorized queue. Immediate alerts are created during submission;
consolidated alerts are persisted in `oversight_notification_queue`.
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.
Production must run the idempotent dispatcher once per minute from exactly one
scheduler instance:
Production must run the idempotent dispatcher once per minute from one scheduler
instance:
```text
* * * * * cd /path/to/shd.build && npm run notifications:oversight:production
@@ -449,27 +608,27 @@ scheduler instance:
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 are excluded from later selection. Existing
retain `delivered_at` evidence, and later queries exclude them. Existing
ISAC notification delivery preferences determine optional browser and email
fan-out after the consolidated inbox message is created.
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`. Edits are recorded under the
`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.
`account.created` is emitted by password, magic-link, and Discord registration.
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.
Immediate definitions are delivered in the registration request. Definitions
with a delay are persisted in `automated_system_message_deliveries`. Production
The registration request delivers Immediate definitions.
`automated_system_message_deliveries` stores definitions with a delay. Production
must run the delayed dispatcher once per minute:
```text