440 lines
22 KiB
Markdown
440 lines
22 KiB
Markdown
# SHD.build architecture
|
||
|
||
The project is built in vertical slices.
|
||
|
||
## Planned 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. Hiding an admin link does
|
||
not grant 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.
|
||
|
||
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
|
||
persistent volume is attached.
|
||
|
||
## 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 first successfully authenticated user is promoted 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.
|
||
That directory is ignored by Git 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. “Revert” is implemented 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
|
||
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
|
||
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.
|
||
|
||
# Moderated game-data contributions
|
||
|
||
ISAC data access is split into `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.
|
||
|
||
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.
|
||
|
||
Every 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
|
||
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 permanently
|
||
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 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,
|
||
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-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`.
|
||
|
||
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.
|
||
|
||
# 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.
|
||
|
||
The three secondary attributes are normalized separately. 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.
|
||
Canonical validation requires four unique slots for T0 and five for T1/T2 for
|
||
every tier at or above the weapon's native tier. 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
`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
|
||
each quality through `weapon_mod_secondary_quality_values`; secondary
|
||
availability is not tied to a numbered slot in canonical data. Mod Effects are
|
||
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 scale linearly. 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.
|
||
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`.
|
||
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
|
||
approved by Oversight.
|
||
|
||
# Skill Chip quality scaling
|
||
|
||
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.
|
||
|
||
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
|
||
assigned abilities has all three active Skill Chip definitions. Skill Chip
|
||
mutations refresh readiness for focuses that reference both the previous and
|
||
resulting ability.
|
||
|
||
Both set-bonus progression systems use the number of equipped High-end chips as
|
||
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 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.
|
||
|
||
# Specialization combat effects 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.
|
||
|
||
`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.
|
||
|
||
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.
|
||
|
||
# Versioned combat calculations
|
||
|
||
`calculation_revisions` supplies one active rule set. Draft revisions can
|
||
change, 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.
|
||
|
||
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.
|
||
|
||
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. Saved and shared builds load the current
|
||
active revision for each request.
|
||
|
||
`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 an operator to review.
|
||
|
||
Rule activation requires `calculation.publish` and an explicit 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.
|
||
|
||
# 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
|
||
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
|
||
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.
|
||
|
||
`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
|
||
|
||
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`.
|
||
|
||
Production must run the idempotent dispatcher once per minute from exactly one
|
||
scheduler instance:
|
||
|
||
```text
|
||
* * * * * cd /path/to/shd.build && npm run notifications:oversight:production
|
||
```
|
||
|
||
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
|
||
ISAC notification delivery preferences determine optional browser and email
|
||
fan-out after the consolidated inbox message is created.
|
||
|
||
## 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_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.
|
||
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
|
||
must run the delayed dispatcher once per minute:
|
||
|
||
```text
|
||
* * * * * cd /path/to/shd.build && npm run notifications:system:production
|
||
```
|
||
|
||
Only one dispatcher instance should run for the embedded database. Delivered
|
||
rows retain both their delivery timestamp and generated notification ID.
|