feat: add community publishing and news syndication
This commit is contained in:
+44
-27
@@ -1,14 +1,20 @@
|
||||
# Community content moderation
|
||||
|
||||
SHD.build applies one moderation lifecycle to public comments, profile
|
||||
biographies, custom avatars, guides, and build descriptions. The shared server
|
||||
service lives in `src/lib/server/community-moderation.ts`. Content save actions
|
||||
must use that service before publication.
|
||||
biographies, custom avatars, and Guides. The shared server service lives in
|
||||
`src/lib/server/community-moderation.ts`. Content save actions must use that
|
||||
service before publication.
|
||||
|
||||
Public database records also host one moderated discussion thread. The thread
|
||||
uses the existing `comment` content type and accepts only visible comments from
|
||||
authenticated Agents. Community moderators may pin one visible comment as the
|
||||
record's Community Note. Pinning and unpinning append `community_note.pinned`
|
||||
and `community_note.unpinned` audit events; a pinned comment remains ordinary
|
||||
community text and follows the same removal rules as every other comment.
|
||||
|
||||
## Publication decisions
|
||||
|
||||
The moderation service combines local rules with the configured provider
|
||||
result:
|
||||
The moderation service combines local rules with the configured provider result:
|
||||
|
||||
- Clean content publishes without human review when automatic publication is
|
||||
enabled.
|
||||
@@ -18,8 +24,8 @@ result:
|
||||
enabled.
|
||||
- An allow term can suppress the matching local term rule. It cannot override a
|
||||
provider flag or threshold.
|
||||
- A user report sends visible content to Oversight. One report does not hide
|
||||
the content because reports can be mistaken or abusive.
|
||||
- A user report sends visible content to Oversight. One report does not hide the
|
||||
content because reports can be mistaken or abusive.
|
||||
|
||||
The service records every automated scan in `content_moderation_scans`.
|
||||
Automated scans do not write to the human ISAC audit log. Human policy changes
|
||||
@@ -33,13 +39,20 @@ Comments use an explicit status:
|
||||
- `pending_review`: hidden while Oversight decides
|
||||
- `removed`: unavailable after author removal or an Oversight decision
|
||||
|
||||
Long-form content uses immutable pending revisions. Profile and guide edits that
|
||||
need review go into `moderated_content_revisions`; the public row keeps its last
|
||||
approved text. Approval copies the reviewed revision into the public row.
|
||||
Rejection preserves the revision and review evidence without publishing it.
|
||||
Long-form content uses immutable pending revisions. Profile biographies keep
|
||||
text candidates in `moderated_content_revisions`. Guides use the complete
|
||||
structured snapshot in `article_revisions`, including title, deck, document,
|
||||
tags, media references, and Build snapshots. The public article keeps its last
|
||||
approved revision while another revision waits for review. Rejection retains the
|
||||
submitted revision and evidence without publishing it.
|
||||
|
||||
Custom avatar uploads remain active after a clean image scan. A flagged image
|
||||
or fail-closed scan error creates an `avatar_reviews` item and is unavailable
|
||||
News articles do not use automated content moderation. The `news.manage`
|
||||
permission gates News creation, editing, publication, and deletion. Comments on
|
||||
News articles remain community content and use the standard comment moderation
|
||||
lifecycle.
|
||||
|
||||
Custom avatar uploads remain active after a clean image scan. A flagged image or
|
||||
fail-closed scan error creates an `avatar_reviews` item and is unavailable
|
||||
through the public avatar route until approval. An Agent report also creates a
|
||||
review item, but leaves an already-published avatar visible while Oversight
|
||||
decides.
|
||||
@@ -69,8 +82,8 @@ applicable, and records a specific ISAC audit event with before and after
|
||||
evidence. Unsaved changes in other categories remain local drafts.
|
||||
|
||||
Site Controls requires `site.settings.manage`. Community Oversight handles text
|
||||
decisions through the existing comment and profile permissions. Avatar
|
||||
Oversight handles custom images through `avatars.review`.
|
||||
decisions through the existing comment and profile permissions. Avatar Oversight
|
||||
handles custom images through `avatars.review`.
|
||||
|
||||
Moderators must record a reason when they approve, reject, remove, restore,
|
||||
dismiss, or resolve a queued item. The action must append an audit event after
|
||||
@@ -78,10 +91,14 @@ the content and review records change.
|
||||
|
||||
## User reports
|
||||
|
||||
Authenticated Agents can report visible content they do not own. The report
|
||||
control must identify the content type and stable content ID, validate that the
|
||||
content remains public, and create a pending `user_content_reports` row.
|
||||
Duplicate pending reports from the same Agent for the same content are rejected.
|
||||
Authenticated Agents can report visible comments, biographies, and avatars they
|
||||
do not own. Registered and anonymous visitors can report visible Guides. A Guide
|
||||
report records the exact published `article_revision_id`; a decision cannot hide
|
||||
a newer revision. Anonymous reports use a random HTTP-only browser token whose
|
||||
domain-separated SHA-256 hash supplies duplicate suppression. The server applies
|
||||
rate limits and requests Turnstile verification after repeated anonymous
|
||||
activity. SHD.build does not use device fingerprinting or retain a raw IP
|
||||
address as the reporter identity.
|
||||
|
||||
A reviewer either dismisses the report and keeps the content or resolves the
|
||||
report through a moderation action. Report details must not copy authentication
|
||||
@@ -89,8 +106,7 @@ secrets or unnecessary personal information into the audit log.
|
||||
|
||||
## Adding a public UGC field
|
||||
|
||||
Guide authoring, build descriptions, and later public text fields must complete
|
||||
this checklist:
|
||||
Guide authoring and later public text fields must complete this checklist:
|
||||
|
||||
1. Normalize and validate the submitted text before scanning.
|
||||
2. Call `moderateCommunityText` with a supported content type.
|
||||
@@ -98,14 +114,15 @@ this checklist:
|
||||
4. Publish only an `approved` result.
|
||||
5. Store held long-form text as an immutable revision so the last approved
|
||||
version remains public.
|
||||
6. Call `recordCommunityModerationScan` with the stable content ID, author, hash,
|
||||
policy version, provider evidence, and local-rule matches.
|
||||
6. Call `recordCommunityModerationScan` with the stable content ID, exact
|
||||
article revision, author, hash, policy version, provider evidence, and
|
||||
local-rule matches.
|
||||
7. Add the content type to the Community Oversight queue and permission checks.
|
||||
8. Add the shared report control to the public rendering.
|
||||
9. Delay follower, owner, or reply notifications until the content becomes
|
||||
public.
|
||||
10. Add approval, rejection, report dismissal, author notification, and audit
|
||||
tests.
|
||||
tests.
|
||||
11. Confirm every public query selects approved content explicitly.
|
||||
|
||||
Do not add a route-specific word filter, provider client, threshold table, or
|
||||
@@ -127,9 +144,9 @@ option, update the legal copy before sending user content under that setting.
|
||||
|
||||
Threshold changes increment the policy version. Stored scans retain their model
|
||||
and policy version so reviewers can interpret older decisions. Test threshold
|
||||
changes against representative Division terminology before activation,
|
||||
including weapon names, damage descriptions, kills, status effects, and build
|
||||
strategy language.
|
||||
changes against representative Division terminology before activation, including
|
||||
weapon names, damage descriptions, kills, status effects, and build strategy
|
||||
language.
|
||||
|
||||
Database migrations that add moderation states or revision storage follow the
|
||||
repository database-preservation procedure: stop this project's development
|
||||
|
||||
+102
-51
@@ -2,8 +2,8 @@
|
||||
|
||||
SHD.build runs in a Node 24 container through the official SvelteKit Node
|
||||
adapter. The image includes application code, migration and notification
|
||||
scripts, and native runtime dependencies. Environment files, databases,
|
||||
backups, and user uploads remain outside the image.
|
||||
scripts, and native runtime dependencies. Environment files, databases, backups,
|
||||
and user uploads remain outside the image.
|
||||
|
||||
## Persistent data
|
||||
|
||||
@@ -16,8 +16,8 @@ mounted at `/app/data`. It contains:
|
||||
- uploaded game-data icons under `game-icons/`.
|
||||
|
||||
Keep this volume during routine deployments. `docker compose down` without
|
||||
`--volumes` preserves it. Before a manual backup or restore, stop the `app`
|
||||
and `scheduler` services because both access the embedded database.
|
||||
`--volumes` preserves it. Before a manual backup or restore, stop the `app` and
|
||||
`scheduler` services because both access the embedded database.
|
||||
|
||||
## Staging environment
|
||||
|
||||
@@ -29,6 +29,21 @@ Staging defaults to:
|
||||
- Passkey relying-party ID: `shd.build`
|
||||
- Adapter canonical origin: `https://stage.shd.build`
|
||||
|
||||
Guide ratings and reports use Cloudflare Turnstile after repeated anonymous
|
||||
activity. Configure both values in each encrypted environment:
|
||||
|
||||
```text
|
||||
TURNSTILE_SITE_KEY=<public site key>
|
||||
TURNSTILE_SECRET_KEY=<server verification secret>
|
||||
```
|
||||
|
||||
Register the exact staging and production hostnames in the Turnstile widget. The
|
||||
browser receives only `TURNSTILE_SITE_KEY`. The server sends a challenge token
|
||||
and the request address to Cloudflare for verification; it never exposes
|
||||
`TURNSTILE_SECRET_KEY` or stores the raw address as visitor identity. Without a
|
||||
valid secret, a visitor who reaches the challenge threshold cannot submit more
|
||||
anonymous ratings or Guide reports.
|
||||
|
||||
Edit its encrypted configuration locally:
|
||||
|
||||
```sh
|
||||
@@ -38,12 +53,11 @@ npm run env:encrypt:staging
|
||||
```
|
||||
|
||||
Add `https://stage.shd.build/auth/discord/callback` to the Discord application's
|
||||
allowed OAuth redirect URIs. Point the staging DNS record at the deployment
|
||||
host and configure the host's TLS reverse proxy to forward
|
||||
`stage.shd.build` to port `5569` on the staging host. Docker publishes this
|
||||
port on the host interfaces so a containerized reverse proxy or tunnel can
|
||||
reach it; firewall the port to trusted LAN/tunnel sources when the host is
|
||||
reachable from other networks.
|
||||
allowed OAuth redirect URIs. Point the staging DNS record at the deployment host
|
||||
and configure the host's TLS reverse proxy to forward `stage.shd.build` to port
|
||||
`5569` on the staging host. Docker publishes this port on the host interfaces so
|
||||
a containerized reverse proxy or tunnel can reach it; firewall the port to
|
||||
trusted LAN/tunnel sources when the host is reachable from other networks.
|
||||
|
||||
The trusted staging runner decrypts `.env.staging` into a mode-`0600` runtime
|
||||
file. Gitea Actions holds the decryption key; Git and the container image do
|
||||
@@ -55,10 +69,10 @@ that signs in instead of writing generated credentials into deployment logs.
|
||||
|
||||
## Gitea Actions runner
|
||||
|
||||
Gitea Actions requires an `act_runner`. Register a trusted runner on the
|
||||
staging host from the repository's **Settings → Actions → Runners** page. The
|
||||
host needs Node/npm, Docker Engine, and Docker Compose v2. The runner account
|
||||
must be able to use Docker and write to the deployment directory.
|
||||
Gitea Actions requires an `act_runner`. Register a trusted runner on the staging
|
||||
host from the repository's **Settings → Actions → Runners** page. The host needs
|
||||
Node/npm, Docker Engine, and Docker Compose v2. The runner account must be able
|
||||
to use Docker and write to the deployment directory.
|
||||
|
||||
Use runner labels that provide an isolated validation job and a host deployment
|
||||
job:
|
||||
@@ -94,9 +108,9 @@ In `https://git.jb.codes/jb/shd.build/settings/actions`:
|
||||
|
||||
`.gitea/workflows/deploy-staging.yml` runs for pushes to `main` and manual
|
||||
dispatches. The trusted staging runner builds each commit-tagged image locally,
|
||||
so the workflow needs no registry credentials. Gitea assigns
|
||||
`gitea.run_number` to `SHD_BUILD_NUMBER`; each new workflow run receives the
|
||||
next integer, while retries of the same run retain their number.
|
||||
so the workflow needs no registry credentials. Gitea assigns `gitea.run_number`
|
||||
to `SHD_BUILD_NUMBER`; each new workflow run receives the next integer, while
|
||||
retries of the same run retain their number.
|
||||
|
||||
## Deployment lifecycle
|
||||
|
||||
@@ -114,12 +128,28 @@ The workflow:
|
||||
The outage covers backup, migration, and container startup. Migrations may also
|
||||
remove records that use retired data formats when a release explicitly ends
|
||||
compatibility. The deployment backup remains the recovery source for those
|
||||
records; the application does not repair or reinterpret them at runtime.
|
||||
The scheduler calls a bearer-authenticated endpoint on the application service
|
||||
once per minute. Scheduled message delivery therefore uses the web process's
|
||||
existing embedded-database connection instead of opening the locked database
|
||||
from a second process. The endpoint runs automated system messages, Oversight
|
||||
consolidation, and email digest checks and is not published through a host port.
|
||||
records; the application does not repair or reinterpret them at runtime. The
|
||||
scheduler calls a bearer-authenticated endpoint on the application service once
|
||||
per minute. Scheduled message delivery therefore uses the web process's existing
|
||||
embedded-database connection instead of opening the locked database from a
|
||||
second process. The endpoint runs automated system messages, Oversight
|
||||
consolidation, email digest checks, and the Ubisoft News importer. Docker does
|
||||
not publish the endpoint through a host port.
|
||||
|
||||
When enabled in production, the Ubisoft importer keeps its own persisted due
|
||||
time, so the one-minute tick starts a feed synchronization no more than once per
|
||||
hour after a successful run. When a synchronization fails, the importer stores
|
||||
the last error and consecutive failure count, then retries after 15 minutes with
|
||||
exponential backoff capped at six hours. Source items that disappear from the
|
||||
feed remain published; the importer performs no automatic removal. The scheduler
|
||||
runs notification delivery before it starts the bounded Ubisoft request, so a
|
||||
slow provider does not postpone notifications for the current tick.
|
||||
|
||||
`deploy/compose.production.yml` declares `UBISOFT_NEWS_IMPORT_ENABLED=true`,
|
||||
while the staging Compose file declares `false`. Development does not set the
|
||||
flag and remains disabled because the importer requires the exact value `true`.
|
||||
The application process evaluates the flag when it handles
|
||||
`/api/internal/scheduler`.
|
||||
|
||||
## Prelaunch operation
|
||||
|
||||
@@ -131,8 +161,8 @@ for a database that does not contain the row:
|
||||
- News, Database, Builds, Guides, and Builder: enabled
|
||||
|
||||
The migration uses an insert-if-missing seed. Later deployments preserve the
|
||||
operator's settings, and configuration changes take effect without a restart.
|
||||
A full environment bootstrap copies the source row. Canonical ISAC game-data
|
||||
operator's settings, and configuration changes take effect without a restart. A
|
||||
full environment bootstrap copies the source row. Canonical ISAC game-data
|
||||
imports leave the target environment's launch settings unchanged.
|
||||
|
||||
Use this operator flow after the first deployment:
|
||||
@@ -149,13 +179,13 @@ Use this operator flow after the first deployment:
|
||||
session before public release. Sign in and verify the same disabled routes
|
||||
remain unavailable.
|
||||
|
||||
Keep Coming Soon enabled while preparing production. Section controls can be
|
||||
set in advance because the launch gate takes precedence for signed-out traffic.
|
||||
Set Account Creation to Disabled if prelaunch access must remain limited to
|
||||
existing identities. Switch the Launch Gate to Public Launch after verification.
|
||||
Disabled sections remain absent from navigation and unavailable by direct URL
|
||||
for every user, including authenticated agents. Authentication bypasses only
|
||||
the Coming Soon gate.
|
||||
Keep Coming Soon enabled while preparing production. Section controls can be set
|
||||
in advance because the launch gate takes precedence for signed-out traffic. Set
|
||||
Account Creation to Disabled if prelaunch access must remain limited to existing
|
||||
identities. Switch the Launch Gate to Public Launch after verification. Disabled
|
||||
sections remain absent from navigation and unavailable by direct URL for every
|
||||
user, including authenticated agents. Authentication bypasses only the Coming
|
||||
Soon gate.
|
||||
|
||||
Turn Coming Soon on to restore the promotional gate. Site-control changes append
|
||||
`system_configuration / site_access.updated` audit events, including the
|
||||
@@ -177,9 +207,9 @@ open while its complete transfer bundle is created:
|
||||
npm run stage:bootstrap:export
|
||||
```
|
||||
|
||||
The command writes a timestamped, ignored JSON bundle under `data/backups/`.
|
||||
The bundle includes user records and password hashes. Transfer it over an
|
||||
encrypted channel, keep it out of Git, then copy it to the staging host and run:
|
||||
The command writes a timestamped, ignored JSON bundle under `data/backups/`. The
|
||||
bundle includes user records and password hashes. Transfer it over an encrypted
|
||||
channel, keep it out of Git, then copy it to the staging host and run:
|
||||
|
||||
```sh
|
||||
/opt/shd-build-stage/bootstrap-staging.sh \
|
||||
@@ -187,11 +217,10 @@ encrypted channel, keep it out of Git, then copy it to the staging host and run:
|
||||
```
|
||||
|
||||
The bootstrap flow stops the app and scheduler, creates a safety backup of the
|
||||
target, verifies that staging is unused, imports the entire development
|
||||
database and uploads, and restarts both services. Delete the transfer copy from
|
||||
the staging host after verifying the imported record counts and critical
|
||||
entities. The copy under the local ignored backup directory remains the
|
||||
recovery source.
|
||||
target, verifies that staging is unused, imports the entire development database
|
||||
and uploads, and restarts both services. Delete the transfer copy from the
|
||||
staging host after verifying the imported record counts and critical entities.
|
||||
The copy under the local ignored backup directory remains the recovery source.
|
||||
|
||||
Because this is a full initial clone, development session tokens and account
|
||||
data also enter staging. Staging and development remain independent from that
|
||||
@@ -293,9 +322,31 @@ Copy the resulting ignored JSON bundle to the deployment host over SSH, then:
|
||||
/secure/path/shd-production-bootstrap-YYYY-MM-DDTHH-MM-SSZ.json
|
||||
```
|
||||
|
||||
The importer verifies the bundle target, creates a production safety backup,
|
||||
and refuses to replace a production database containing users, builds, audit
|
||||
events, or proposals. Remove the remote transfer copy after verification.
|
||||
The importer verifies the bundle target, creates a production safety backup, and
|
||||
refuses to replace a production database containing users, builds, audit events,
|
||||
or proposals. Remove the remote transfer copy after verification.
|
||||
|
||||
### Promote one News article
|
||||
|
||||
Export a locally authored News article by ID or slug while the development
|
||||
server is stopped:
|
||||
|
||||
```sh
|
||||
npm run article:export -- <article-id-or-slug>
|
||||
```
|
||||
|
||||
The ignored bundle contains the article draft, immutable revisions, search row,
|
||||
managed-image metadata, and image bytes. It excludes comments, reports, user
|
||||
records, Builds, and ISAC data.
|
||||
|
||||
After deploying the matching application schema, stop the production app and
|
||||
scheduler and mount the transferred bundle into a one-off application container.
|
||||
Run `scripts/article-transfer.mjs import` with `ARTICLE_IMPORT_ENABLED=true` and
|
||||
`--confirm-article-import`. The importer creates a production database backup
|
||||
before opening the database, resolves the article owner by stable ID or
|
||||
username, rejects ID and slug conflicts, verifies image checksums, and appends
|
||||
`editorial_content / news.promoted`. Restart and health-check both services,
|
||||
then remove the transferred bundle from the host.
|
||||
|
||||
### Pull production ISAC data into development
|
||||
|
||||
@@ -312,10 +363,10 @@ notifications, settings, and audit history untouched.
|
||||
|
||||
### Pull production ISAC data into staging
|
||||
|
||||
Manually run **Replace staging ISAC data from production** in Gitea Actions.
|
||||
The workflow exports a consistent production snapshot, restarts production,
|
||||
then stops staging and imports it. The staging importer creates full and
|
||||
ISAC-only safety backups before replacement. Deployments do not run this flow.
|
||||
Manually run **Replace staging ISAC data from production** in Gitea Actions. The
|
||||
workflow exports a consistent production snapshot, restarts production, then
|
||||
stops staging and imports it. The staging importer creates full and ISAC-only
|
||||
safety backups before replacement. Deployments do not run this flow.
|
||||
|
||||
### Promote managed images without replacing other data
|
||||
|
||||
@@ -333,8 +384,8 @@ internal scheduler. `REMOVE_BG_CREDIT_CHECK_HOURS` controls the account lookup
|
||||
interval and defaults to six hours. Low-capacity alerts require both remaining
|
||||
pools to cross their thresholds: `REMOVE_BG_LOW_CREDIT_THRESHOLD` defaults to
|
||||
five paid credits, and `REMOVE_BG_LOW_FREE_CALL_THRESHOLD` defaults to ten free
|
||||
calls. Alerts are mandatory ISAC notifications for active Administrators and
|
||||
are deduplicated for each warning or critical episode.
|
||||
calls. Alerts are mandatory ISAC notifications for active Administrators and are
|
||||
deduplicated for each warning or critical episode.
|
||||
|
||||
```sh
|
||||
npm run images:export
|
||||
@@ -368,8 +419,8 @@ adds new stable IDs, skips identical assets, and blocks conflicting records.
|
||||
After reviewing an intentional metadata or file replacement, add
|
||||
`--update-existing`. Imported changes append `image.imported` or
|
||||
`image.import_updated` audit events. Remove the transferred bundle after
|
||||
verifying both environments because it may contain unpublished visual assets
|
||||
and creator identifiers.
|
||||
verifying both environments because it may contain unpublished visual assets and
|
||||
creator identifiers.
|
||||
|
||||
The `.env.staging` and `.env.production` npm import commands are intended for
|
||||
maintenance performed directly in an environment where those database and
|
||||
|
||||
+942
-858
File diff suppressed because it is too large
Load Diff
+289
-207
@@ -5,24 +5,28 @@ 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
|
||||
- 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.
|
||||
- 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
|
||||
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.
|
||||
|
||||
@@ -49,9 +53,9 @@ 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
|
||||
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.
|
||||
|
||||
@@ -84,22 +88,102 @@ 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.
|
||||
Comments, profile biographies, custom avatars, and Guides share the moderation
|
||||
lifecycle documented in [`CONTENT_MODERATION.md`](CONTENT_MODERATION.md). Local
|
||||
rules and the configured provider classify each submission before publication.
|
||||
Clean submissions can publish without human work. Flagged submissions and
|
||||
fail-closed provider errors enter the appropriate Oversight queue.
|
||||
|
||||
Agents may report visible content they do not own. Reports enter Oversight
|
||||
without hiding the content. Human decisions require a reason and append an
|
||||
audit event. Long-form edits that need review use immutable pending revisions
|
||||
so the last approved biography, guide, or description remains public.
|
||||
without hiding the content. Human decisions require a reason and append an audit
|
||||
event. Long-form edits that need review use immutable pending revisions so the
|
||||
last approved biography or Guide remains public.
|
||||
|
||||
ISAC Site Controls owns provider behavior, content scopes, thresholds, link and
|
||||
PII checks, and allowed or banned terms. API credentials remain in server
|
||||
environment configuration.
|
||||
|
||||
## Guides and News articles
|
||||
|
||||
`articles` stores the stable identity, kind, owner, immutable slug, visibility,
|
||||
and published or pending revision pointers for Guides and News. Authors edit one
|
||||
mutable `article_drafts` row with optimistic version checks. Publication creates
|
||||
an immutable `article_revisions` snapshot. Public readers and search load only
|
||||
the revision named by `articles.published_revision_id`.
|
||||
|
||||
Guides require OpenAI moderation before an unlisted or public revision can
|
||||
publish. The moderation request includes the title, deck, document text, image
|
||||
alternative text and captions, cover image, and inline images. A failed provider
|
||||
request stays in Guide Oversight. Clean revisions may publish automatically;
|
||||
flagged revisions preserve the previous public revision while Oversight decides.
|
||||
News bypasses this lifecycle because `news.manage` is the editorial gate. News
|
||||
comments still use community moderation.
|
||||
|
||||
### Ubisoft News syndication
|
||||
|
||||
The Ubisoft News importer reads the English-language Division Resurgence feed
|
||||
from Ubisoft Nimbus. It stores each Nimbus item ID as the stable external ID and
|
||||
publishes only the source title, excerpt, and canonical Ubisoft link. It does
|
||||
not copy the source article body or images. Imported articles use Ubisoft's
|
||||
original publication timestamp rather than the import time.
|
||||
|
||||
`external_news_sources` maps each Nimbus ID to one SHD.build article. A SHA-256
|
||||
content hash covers the normalized title, excerpt, canonical URL, and source
|
||||
publication timestamp. An unchanged hash updates `last_seen_at` without creating
|
||||
a revision. A changed hash creates and publishes an immutable revision. The
|
||||
importer does not delete or unpublish an article when Nimbus omits it from a
|
||||
later feed, and it does not restore an article that an editor deleted. The
|
||||
Newsroom treats source-linked articles as read-only so local edits cannot retain
|
||||
Ubisoft attribution. The importer defers future-dated items until their source
|
||||
publication time.
|
||||
|
||||
The seeded, non-public `Ubisoft Official News` service identity owns imported
|
||||
articles and acts on their audit events. A first import records
|
||||
`editorial_content / news.imported`; a later content sync records
|
||||
`editorial_content / news.import_synced`. Audit snapshots identify the provider,
|
||||
Nimbus ID, canonical URL, source timestamp, and resulting revision.
|
||||
|
||||
The existing bearer-authenticated scheduler endpoint invokes the importer. A
|
||||
successful run sets the next check one hour later. The importer stores the last
|
||||
attempt, last success, next check, consecutive failure count, and bounded last
|
||||
error in provider state. Failures retry after 15 minutes with exponential
|
||||
backoff capped at six hours. A successful run clears the error and failure
|
||||
count.
|
||||
|
||||
The article document is a versioned, strictly validated Tiptap JSON envelope.
|
||||
The application stores no author-supplied HTML and renders accepted nodes with
|
||||
Svelte components. `article_revision_build_embeds` supports several Builds per
|
||||
Guide and several Guides per Build. Each relation records a bounded publication
|
||||
snapshot. Authors may embed any Build they own; another Agent's Build must be
|
||||
public and becomes unavailable in the Guide if its source stops being public.
|
||||
|
||||
Guide taxonomy combines fixed activity and difficulty terms with active
|
||||
Specializations, Focuses, and weapon categories. `article_search` stores the
|
||||
published title, deck, body text, and tags. Weighted token scoring prioritizes
|
||||
title, deck, tags, then body because the embedded Turso engine does not ship the
|
||||
FTS5 module. Search rows exist only for current public revisions.
|
||||
|
||||
Registered and anonymous visitors may rate a Guide from 1 through 5. Anonymous
|
||||
ratings and Guide reports use a random HTTP-only browser token; the database
|
||||
stores only its domain-separated hash. Repeated anonymous activity requires a
|
||||
Turnstile result. Guide reports reference the exact published revision so a
|
||||
report against an older revision cannot hide newer work.
|
||||
|
||||
The Builder stores loadout data only. It has no summary or Guide body. An owned
|
||||
Build page links to `/guides/new?build=<id>`, where the server verifies
|
||||
ownership and initializes a removable Build block in a new private Guide.
|
||||
|
||||
## Public data corrections and discussion
|
||||
|
||||
Every public database card links to its canonical ISAC record with an exact
|
||||
`item` ID and offers a moderated Community Discussion. Authenticated Agents can
|
||||
post notes and replies; Community Oversight may pin one visible comment as the
|
||||
record's Community Note. A public correction opens the same ISAC editor used by
|
||||
data operators. Contributor-mode submissions create a proposal rather than
|
||||
changing canonical data. Contributors can attach an optional bounded rationale;
|
||||
the rationale is retained with the proposal and visible to Data Oversight, but
|
||||
does not become part of the canonical action replay.
|
||||
|
||||
## Increment plan
|
||||
|
||||
1. Product shell and anonymous local-draft builder.
|
||||
@@ -109,10 +193,11 @@ environment configuration.
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -134,8 +219,8 @@ 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.
|
||||
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:
|
||||
|
||||
@@ -144,18 +229,18 @@ 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.
|
||||
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.
|
||||
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
|
||||
@@ -165,22 +250,22 @@ deployment, backup, synchronization, and maintenance safeguards documented in
|
||||
|
||||
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.
|
||||
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.
|
||||
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
|
||||
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.
|
||||
@@ -189,47 +274,48 @@ mutation rather than deletion or modification of the original 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.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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
@@ -241,10 +327,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 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.
|
||||
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
|
||||
@@ -252,8 +338,8 @@ 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.
|
||||
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
|
||||
|
||||
@@ -266,8 +352,8 @@ 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.
|
||||
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
|
||||
@@ -286,14 +372,14 @@ 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.
|
||||
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
|
||||
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.
|
||||
edit adds the Tier II projection and mechanic values without changing the weapon
|
||||
reference.
|
||||
|
||||
# OS Protocol progression
|
||||
|
||||
@@ -309,22 +395,22 @@ 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.
|
||||
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
|
||||
`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.
|
||||
`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
|
||||
@@ -336,62 +422,60 @@ 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.
|
||||
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`.
|
||||
`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,
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
`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.
|
||||
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.
|
||||
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
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -413,8 +497,8 @@ 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.
|
||||
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.
|
||||
@@ -457,24 +541,23 @@ separate event from changes to the Agent's current Extra Health state.
|
||||
The optional Action Family catalog provides authoring recipes for common
|
||||
mechanics. Applying a recipe copies its typed scenario inputs, state pools, and
|
||||
clauses into the source-owned program. Those branches become editable
|
||||
authoritative data; the recipe key remains provenance, not a runtime
|
||||
dependency. Adding another recipe appends branches. Replacing a program with a
|
||||
recipe requires confirmation because replacement discards the current branch
|
||||
graph.
|
||||
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
|
||||
`CONFIG REQUIRED`, and calculations exclude it. Changing readiness to `ready` or
|
||||
`requires_assumptions` restores strict branch, numbered-outcome, and
|
||||
description-placeholder validation.
|
||||
|
||||
The program graph uses three child tables:
|
||||
|
||||
- `conditional_effect_state_pools` defines agent- or target-scoped flags,
|
||||
counts, stacks, durations, scalar values, marks, and resources. Each pool
|
||||
owns its bounds, expiry policy, refresh policy, and best-case value.
|
||||
- `conditional_effect_clauses` listens for one event from the registry. A
|
||||
clause owns a typed predicate, reducer, optional cooldown, optional weight
|
||||
counts, stacks, durations, scalar values, marks, and resources. Each pool owns
|
||||
its bounds, expiry policy, refresh policy, and best-case value.
|
||||
- `conditional_effect_clauses` listens for one event from the registry. A clause
|
||||
owns a typed predicate, reducer, optional cooldown, optional weight
|
||||
expression, state mutations, and optional recipe provenance. Reducers convert
|
||||
event streams into counts, distinct or consecutive matches, sliding windows,
|
||||
durations, intervals, or derived values. Mutations can set, add, consume,
|
||||
@@ -486,17 +569,17 @@ The program graph uses three child tables:
|
||||
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.
|
||||
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.
|
||||
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
|
||||
@@ -508,9 +591,9 @@ 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.
|
||||
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
|
||||
@@ -543,8 +626,8 @@ formatter regenerates the listing after each change.
|
||||
|
||||
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
|
||||
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
|
||||
@@ -569,12 +652,12 @@ 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 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:
|
||||
|
||||
@@ -587,15 +670,15 @@ The model returns one structured result:
|
||||
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.
|
||||
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,
|
||||
@@ -616,11 +699,11 @@ 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.
|
||||
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
|
||||
|
||||
@@ -628,16 +711,16 @@ 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.
|
||||
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,
|
||||
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.
|
||||
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.
|
||||
@@ -657,8 +740,8 @@ 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
|
||||
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
|
||||
@@ -673,8 +756,8 @@ 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.
|
||||
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.
|
||||
@@ -689,8 +772,8 @@ estimate.
|
||||
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.
|
||||
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
|
||||
@@ -734,11 +817,10 @@ 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.
|
||||
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
|
||||
@@ -751,12 +833,12 @@ 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.
|
||||
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
|
||||
|
||||
@@ -793,9 +875,9 @@ 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.
|
||||
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
|
||||
@@ -820,8 +902,8 @@ keeps the library metadata and one copy of each file.
|
||||
|
||||
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;
|
||||
`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
|
||||
@@ -831,12 +913,12 @@ instance:
|
||||
* * * * * 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.
|
||||
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
|
||||
|
||||
@@ -847,15 +929,15 @@ managed through ISAC → System Configuration → Automated Messages by agents w
|
||||
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
|
||||
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:
|
||||
`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
|
||||
|
||||
Reference in New Issue
Block a user