Files
shd.build/docs/architecture.md
T

855 lines
45 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.
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, guides, and build descriptions
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, guide, or description 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.
## 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 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. Each protocol Talent owns its
mechanic program through the shared conditional-effect tables.
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.
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 the shared
level-1 and level-20 primary roll endpoints for each quality; primary values
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
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 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.
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.
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
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 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.
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.
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.
The `game_data` audit stream captures Skill Chip mutations, owned relationships,
and safe icon metadata.
# 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 builder supplies normalized timeline events to each equipped mechanic.
`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.
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.
Builder 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 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 event registry publishes authoritative
field metadata where branches need a bounded contract. For example,
`weapon_hit.values.headshot` is a boolean that distinguishes headshots from
body shots.
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 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. 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.
- `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. Restoration
outcomes may deliver their total immediately or continuously over an explicit
duration.
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.
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. 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 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.
## 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 affects loadout results.
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 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. 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.
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 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
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 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, 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
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.
Production must run the idempotent dispatcher once per minute from 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 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. 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.