§16 planned deep animation keys and a bulk-fill switch. Measuring first changed the phase: a complete one-direction animation set is 174,453 frames / 281.5 MB (not the ~119,000 / ~117 MB §11 estimated), every frame carries its own centre and 23,818 of 26,274 actions have frames of differing size — and the site displays still pictures. So none of it was built (§11.2, org lead 2026-09-11); `body/<id>/a<n>/f<k>` stays named in §5 and refused in practice. §4.10 is what the walk found instead. A body's actions are a contiguous band and the next body's begin immediately after, so one action past the ceiling is a real record of another creature: 643 of 795 legacy bodies pass every check there and 452 are byte-identical to body+1's action 0. `Animations.GetAnimLength` is not the ceiling — it disagrees with the index arithmetic on one body of this client, by nine actions. What shipped: the 73 bodies with no art at action 0 (a horse at `body/820/a23`), the catalogue key carrying its action, the atlas join that reads it instead of hardcoding `a0`, and the ceiling that makes the fallback walk safe. Catalogue 1,022 -> 1,095; EXTRACTOR_VERSION 2 -> 3; protocol stays 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
344 lines
24 KiB
Markdown
344 lines
24 KiB
Markdown
# module-uo — the tables it owns
|
|
|
|
The 27 tables `module-uo` creates and owns, and the reasoning behind their shapes. **26 `shard_*`
|
|
plus `uo_link_config`**, all created by the idempotent `server/db/schema.sql` fragment core replays
|
|
after its own schema on every boot, and all dropped by `server/db/purge.sql` when an operator purges
|
|
the module's data.
|
|
|
|
This page moved out of [`BACKEND_DESIGN.md`](../../website/BACKEND_DESIGN.md) §3 when Phase 4 closed
|
|
([`MODULE_SYSTEM.md`](../../website/MODULE_SYSTEM.md) §2.7.2): core's schema reference describes
|
|
core's tables, and these are not core's. The text is unchanged — where it says "the shard", it means
|
|
the Ultima Online shard this module fronts. Core's own module bookkeeping table,
|
|
`installed_modules`, stays in `BACKEND_DESIGN.md` where it belongs.
|
|
|
|
The `shard_` and `uo_link_` prefixes are **grandfathered**
|
|
([`MODULE_API.md`](../../website/MODULE_API.md) §6.5). A new module's tables are prefixed with its
|
|
own id; these predate the rule, and they are read by the shipped Android app, so renaming them would
|
|
be a data migration plus a client break.
|
|
|
|
**Not every table has a section here.** The pre-3.0 ingest tables (`shard_events`, `shard_online`,
|
|
`shard_economy`, `shard_houses`, `shard_champs`, `shard_guilds`, `shard_governors`,
|
|
`shard_governor_terms`, `shard_presence`, `shard_pages`, `shard_account_links`, `uo_link_config`)
|
|
are described where their wire frames are — [`link/PLAN.md`](../../link/PLAN.md) §5 and
|
|
[`link/INTEGRATION.md`](../../link/INTEGRATION.md). What follows is everything that carried a
|
|
design note worth keeping.
|
|
|
|
---
|
|
|
|
## shard_ruleset — the shard's published ruleset (Protocol 3.0)
|
|
|
|
Singleton row (`id = 1`, CHECK-constrained) holding the latest `world.ruleset` frame: `rev`,
|
|
`expansion`, `payload` JSON (the whole frame), `t`, `updated_at`. The shard re-emits the complete
|
|
ruleset on every sidecar connect, so this is an **overwrite, not an append** — and the kind is
|
|
deliberately **not** in `LOGGED_KINDS`, since logging it would put a duplicate row in `shard_events`
|
|
on every reconnect while `server.hello` already marks each of those.
|
|
|
|
The frame is stored whole rather than normalized into columns: it is a flat description of server
|
|
config that is read as one page, so splitting it up would mean a schema change every time the shard
|
|
grows a new block. `rev` (the shard's FNV-1a of the body) and `expansion` are hoisted only because
|
|
they are cheap to display — the same payload-plus-hoisted-columns shape `shard_champs` uses.
|
|
|
|
**No row means the shard has never published one** (an older plugin, or `Bridge.RulesetEnabled=false`),
|
|
served as `null` rather than `{}`: "not published yet" and "published, everything off" are different
|
|
answers and the page renders them differently.
|
|
|
|
## shard_points_boards — points / loyalty leaderboards (Protocol 3.0)
|
|
|
|
One row per point system, keyed by the shard's own `PointsType` name (`QueensLoyalty`,
|
|
`CleanUpBritannia`, …). The shard carries ~25 of these, each a standing players build over months.
|
|
Columns: `system` (PK), `name`, `name_cliloc`, `max_points`, `players`, `show_on_gump`, `payload` JSON
|
|
(the whole `points.board` frame), `t`, `updated_at`.
|
|
|
|
**The top-N list stays inside `payload`** rather than being normalized into a `shard_points_entries`
|
|
table. It is a fixed-size list (10 by default) that is only ever read whole — exactly like
|
|
`shard_governors.candidates` — so normalizing buys nothing until something needs a per-character
|
|
reverse lookup, and a character's own standings already ride inside `char.profile` instead.
|
|
|
|
Board state, not events: `points.board` is **not** in `LOGGED_KINDS`, for the same reason
|
|
`guild.update` isn't. The shard emits a frame every time anyone's score moves a top ten, so logging
|
|
would grow `shard_events` without bound for something whose only interesting value is its latest
|
|
version. There is also **no delete path** — the shard's set of systems is fixed at startup, so there is
|
|
no `points.remove` to mirror.
|
|
|
|
Two values carry non-obvious meanings, both set by the plugin and both documented in
|
|
[`link/INTEGRATION.md`](../../link/INTEGRATION.md) §4:
|
|
|
|
- **`max_points = 0` means uncapped**, and on a real shard that is the *common* case (ServUO's
|
|
uncapped idiom is `double.MaxValue`, which the plugin normalises to 0). Anything rendering
|
|
`points / max_points` must special-case it.
|
|
- **`name` is usually NULL**, with `name_cliloc` set instead — most systems name themselves with a
|
|
cliloc rather than a literal. Listing therefore orders by `COALESCE(name, system)`, so boards
|
|
awaiting cliloc resolution sort by their own key rather than clumping together under NULL.
|
|
|
|
## shard_vendors / shard_vendor_items — the player-vendor marketplace (Protocol 3.0)
|
|
|
|
The shard-wide shop index, fed by `vendor.listing` / `vendor.listing.remove`. One row per player
|
|
vendor and one per priced listing. Full operator detail in [`MARKETPLACE.md`](../../website/MARKETPLACE.md); the
|
|
design is `docs/link/v3.md` §8.
|
|
|
|
| Table | Shape |
|
|
|---|---|
|
|
| `shard_vendors` | `serial` (PK), `shop_name`, `owner_serial`, `owner_name`, `map`/`x`/`y`/`z`, `region`, `house`, `item_count`, `item_total`, `truncated`, `t`, `updated_at`. Indexes on owner, map, region and `updated_at`. |
|
|
| `shard_vendor_items` | `id` (PK), `vendor_serial`, `serial`, `item_id`, `hue`, `amount`, `price`, `name`, `cliloc`, `display_name`, `child`. Indexes on `vendor_serial`, `price`, `item_id`, `display_name`, and `(display_name, price)`. |
|
|
|
|
**Ingest is per-vendor and authoritative**: the frame is the whole shop, so ingest is
|
|
delete-then-insert of that vendor's listings inside one transaction. All-or-nothing matters
|
|
specifically because the two writes are "the shop" and "what is in it" — a failure between them
|
|
leaves a shop advertising an inventory it no longer has, which is visibly wrong and indistinguishable
|
|
from a genuinely empty shop. No foreign keys, consistent with every other `shard_*` table.
|
|
|
|
**There is deliberately no `payload` column**, unlike `shard_points_boards` directly above. The
|
|
board's top-N is a fixed-size list read whole, so it lives in JSON; here the items *are* the
|
|
searchable rows, so they are normalized and nothing is left worth duplicating. The sidecar keeps the
|
|
whole blob — outage resilience is its job, search is ours.
|
|
|
|
Market state, not events: neither kind is in `LOGGED_KINDS`, and this is the strongest case of the
|
|
three v3 kinds. One frame carries up to 250 listings and the sweep re-emits a shop on any price
|
|
change, so logging would turn `shard_events` into a price history nobody reads.
|
|
|
|
Two columns carry non-obvious meanings:
|
|
|
|
- **`item_count` vs `item_total`.** `item_count` is what the frame published; `item_total` is what
|
|
the shop actually holds. They differ when `truncated` — the shard caps listings per frame
|
|
(`Bridge.MarketMaxListings`, 250 by default), and a commodity reseller with thousands of stacks
|
|
genuinely exceeds it. Any UI must show both or it presents a partial shop as complete.
|
|
- **`display_name` is denormalized at ingest**, resolved from the item's literal `name` (preferred —
|
|
a player set it, so it is more specific) else its `cliloc` against `shard_clilocs`. Resolving at
|
|
query time would put the cliloc table on the hot path and make search-by-name impossible. Because
|
|
the shard's diff sweep will not re-send an unchanged shop just because the site learned what its
|
|
items are called, **a cliloc import triggers a bulk re-resolution** of this column (after a boot
|
|
import and after an admin import; ~50 ms per thousand rows, never throws).
|
|
|
|
`updated_at` is written explicitly on every upsert rather than left to `ON UPDATE CURRENT_TIMESTAMP`,
|
|
which MariaDB does not fire when every column is written back unchanged. A shop re-published
|
|
identically is still *freshly confirmed*, and without this the staleness banner would age a perfectly
|
|
current shop forever.
|
|
|
|
## shard_feature_visibility — per-feature audience config (Protocol 3.0)
|
|
|
|
One row per shard feature: `feature` (PK), `enabled`, `audience` (a rung on the ladder in [`API.md`](API.md)),
|
|
`stream` (whether the feature's kinds fan out over SSE at all), `field_rules` JSON (`{field: rung}`
|
|
for the sensitive fields only), `updated_by`, `updated_at`.
|
|
|
|
**An absent row means "use the compiled default", and the compiled defaults reproduce pre-3.0
|
|
behavior — so an empty table is a no-op and there is nothing to seed.** Stored rows are merged over
|
|
the defaults on read, which is also where the invariants are re-applied: a row naming an unknown
|
|
feature is ignored (a stale row must not resurrect a removed feature), an invalid rung falls back to
|
|
the default rather than failing open, and a rule touching a locked field (`acct` / `webId`) is
|
|
discarded. See [`API.md`](API.md).
|
|
|
|
## shard_spawn_* / shard_regions / shard_landmarks / shard_champion_spawns / shard_atlas_meta — the spawn atlas (Protocol 3.0)
|
|
|
|
Static shard **content**, not live shard state. Nothing here comes from the sidecar: the atlas is
|
|
derived from the shard's own ServUO tree, re-read on **every server boot** and hash-gated so an
|
|
unchanged tree costs one read pass and no write. Nothing is precomputed and committed — a shard's
|
|
maps change over its life, and a snapshot in the repo would silently drift from the world players
|
|
actually see. These tables stay populated whether the shard is up or not. Full operator detail in
|
|
[`SPAWN_ATLAS.md`](../../website/SPAWN_ATLAS.md); the design is `docs/link/v3.md` §6.
|
|
|
|
**No facet name appears anywhere in the code.** A shard may add facets, replace them, or rename them
|
|
when its maps are updated; the facet set is discovered from the tree, and the loose spellings in
|
|
`Data/Locations` are matched against it rather than looked up in a table.
|
|
|
|
| Table | Key columns |
|
|
|---|---|
|
|
| `shard_spawn_creatures` | `slug` PK, `name`, `total`, `points`, `facets` JSON, `art` NULL |
|
|
| `shard_spawn_points` | `id` PK, `facet`, `name`, `x`, `y`, `width`, `height`, `spawn_range`, `max_count`, `min_delay`, `max_delay`, `tod_start/end/mode`, `region`, `landmark`, `label` |
|
|
| `shard_spawn_point_types` | `(point_id, slug)` PK, `max_count` |
|
|
| `shard_regions` | `facet`, `name`, `type`, `priority`, `parent`, `rects` JSON |
|
|
| `shard_landmarks` | `facet`, `name`, `grp`, `x`, `y`, `z` |
|
|
| `shard_champion_spawns` | `slug` PK, `name`, `grp`, `type`, `random_type`, `facet`, `x`, `y`, `z`, `radius`, `label` |
|
|
| `shard_atlas_meta` | Singleton (`id = 1`), `payload` JSON (counts, a sha256 per source file, `parserVersion`), `imported_at` |
|
|
| `shard_atlas_pending` | Singleton (`id = 1`), `status` (`pending`/`rejected`), `payload` JSON, `detected_at` |
|
|
|
|
The first seven are **import-owned**: a refresh empties and reloads every one inside a single
|
|
transaction, so a failed reload leaves the previous atlas intact rather than a half-loaded world.
|
|
Nothing else writes to them, and nothing holds a foreign key to them — no FKs at all, consistent with
|
|
every other `shard_*` table.
|
|
|
|
**`shard_atlas_pending` is the security-relevant one.** A refresh that would REMOVE a facet is never
|
|
applied automatically: facet loss is indistinguishable at boot from a half-copied or mid-update tree,
|
|
so it is staged here for an admin to approve or reject, and **startup is never blocked by it**. Only
|
|
the decision is stored — source hashes plus the facet diff, a few KB — and approving re-parses the
|
|
tree, so a multi-megabyte blob never lands in the database and what gets applied matches the tree at
|
|
approval time. A rejection is remembered against those exact hashes so a declined refresh does not
|
|
re-prompt on every restart. Everything else (new facets, renamed regions, changed spawns) applies
|
|
immediately, since none of it can destroy data an operator would miss.
|
|
|
|
The boot refresh is **best-effort by contract**: no configured path, an unreadable mount, a malformed
|
|
file or a database error is caught and logged, and the site comes up serving whatever atlas it had.
|
|
The tree path comes from the `spawn_atlas_servuo_path` setting, falling back to `SERVUO_PATH`.
|
|
|
|
**A refresh re-derives when the tree changed OR the parser did.** `spawnAtlasSource.PARSER_VERSION`
|
|
is stored in `shard_atlas_meta` beside the source hashes and bumped whenever the parser produces
|
|
different data from identical files. Hashing the tree alone would strand an install whose maps never
|
|
change on whatever an older build derived — a corrected parse would ship and never reach the data.
|
|
|
|
Four column choices worth stating, because each one is a trap:
|
|
|
|
- **`spawn_range`, not `range`**, and **`grp`, not `group`** — both are reserved words.
|
|
- **`DELETE`, not `TRUNCATE`.** `TRUNCATE` is DDL in MariaDB and implicitly commits, which would
|
|
defeat the all-or-nothing reload. At ~7k rows the difference does not matter.
|
|
- **Point ids are assigned explicitly**, not left to `AUTO_INCREMENT`: the `shard_spawn_point_types`
|
|
rows need to know them, and `conn.batch()` reports no usable `insertId` for a multi-row insert.
|
|
- **Plain `INDEX` on `name`, deliberately not `FULLTEXT`.** ~800 creature rows makes a `LIKE` scan
|
|
free, and FULLTEXT's minimum token length would break searches for names like "orc".
|
|
|
|
`shard_champion_spawns` is the *configured* altar roster ("there is an Unholy Terror altar in
|
|
Deceit"). The live `champ.update` feed in `shard_champs` is the separate answer to "it is on level 3
|
|
right now". Both exist; they are not the same data.
|
|
|
|
**`shard_spawn_creatures.art` is DERIVED, never written by the atlas import itself** — see
|
|
`shard_assets` below. The project still ships no creature artwork: sprites live in the operator's own
|
|
client `.mul`/`.uop` files and are theirs, not ours to redistribute. What changed in Protocol 8 is
|
|
who extracts them: the shard does, from its own client, over the bridge. An operator's hand-drawn map
|
|
plus images under the (already gitignored) `server/uploads/atlas/` still wins over anything imported.
|
|
Text-only is still the normal, supported state — an install with no shard link never imports one, and
|
|
even a complete import leaves two thirds of the playable ghost and gargoyle bodies without art.
|
|
|
|
## shard_assets / shard_creature_bodies / shard_asset_meta — the Asset Bridge (Protocol 8)
|
|
|
|
Creature artwork read from the shard's own UO client ([`link/v8.md`](../../link/v8.md) §6, §8, §12).
|
|
|
|
| Table | Shape |
|
|
|---|---|
|
|
| `shard_assets` | `asset_key` VARCHAR PK (§5's key, e.g. `body/34/a0`, `body/820/a23`, `static/3922/h33`, `land/3`), `family`, `sha256`, `bytes`, `width`, `height`, `body`, `action`, `direction`, `file`, `catalog`, `imported_at` |
|
|
| `shard_creature_bodies` | `slug` PK, `type_name` (the ServUO class name asked), `body` nullable, `status`, `resolved_at` |
|
|
| `shard_asset_meta` | Singleton (`id = 1`), `payload` JSON (catalogue id, extractor version, source fingerprint, counts), `imported_at` |
|
|
|
|
**These are the one part of this schema that is deliberately NOT import-owned**, and the reason is
|
|
the atlas tables sitting directly above them. `replaceAtlas` empties and refills
|
|
`shard_spawn_creatures` on every refresh, and a refresh runs on every boot; an imported filename
|
|
stored on that row would be destroyed by an ordinary re-parse of the ServUO tree, with the next asset
|
|
Update finding the client files unchanged, reporting "nothing to do", and never restoring it. So the
|
|
assets live out here, upserted per key, and `replaceAtlas` re-derives `art` from them on the way
|
|
past — `{ ...derived, ...operatorMap }`, which is the one place "the operator's map wins" is
|
|
enforced.
|
|
|
|
Four details that are load-bearing rather than incidental:
|
|
|
|
- **`file` is a filename under the uploads directory, never a path**, and it is content-addressed
|
|
(`uo-body-34-a0-<sha8>.png`). A stable name overwritten in place would leave every browser and CDN
|
|
serving the previous client's sprite from cache, with the row perfectly correct.
|
|
- **The `art` derivation joins on the catalogue key**, `a.asset_key = CONCAT('body/', b.body, '/a0')`,
|
|
not `a.body = b.body`. The simpler join is correct today and stops being correct the moment deeper
|
|
animation keys (`body/400/a2/f0`) arrive, at which point one slug matches dozens of rows.
|
|
- **`shard_creature_bodies` IS replaced whole**, unlike `shard_assets`: it is derived from the atlas's
|
|
creature list, so a slug that has left the atlas has no meaning, and the pass that rebuilds it is a
|
|
shard round trip rather than a file transfer.
|
|
- **`status` keeps the negative answers** — `unknown` (the spawn files name a type this shard's
|
|
scripts do not define, which is real drift), `notCreature` (a spawn entry for an item or
|
|
decoration, a permanent answer), `failed`. Without them the next pass asks again, and each name
|
|
costs a real constructor on the shard's Core thread.
|
|
|
|
`shard_spawn_creatures.name` already holds the ServUO **class name** — the atlas build picks the
|
|
winning spelling of the spawn type token rather than inventing a display label — which is why the
|
|
body pass needs no extra column to ask its question.
|
|
|
|
### Item and land art: the same table, a different shape of use (phase 5)
|
|
|
|
The creature catalogue is a **set**: one manifest walk covers every key, so one stored fingerprint in
|
|
`shard_asset_meta` describes all of them and an Update is a hash diff. Item art has no set — the
|
|
client addresses 49,152 item graphics times 3,000 hues — so those rows arrive one at a time, because
|
|
something on this site named the key.
|
|
|
|
Three consequences in this schema:
|
|
|
|
- **`catalog` is per row**, and it is what phase 5 added. It records the shard's art catalogue id (a
|
|
hash of `artLegacyMUL.uop`/`art.mul`, `hues.mul`, `tiledata.mul`, `verdata.mul` and its extractor
|
|
version), so staleness is a column comparison rather than a manifest diff. A client patch changes
|
|
it; a restart does not. NULL means "written before the column existed", which is stale by the same
|
|
test and costs one re-fetch. The body import fills it too, so one column answers the question
|
|
everywhere.
|
|
- **`shard_asset_meta` stays the body catalogue's alone.** A warm pass writing there would tell the
|
|
body import that a client it never looked at is unchanged, and the creature catalogue would quietly
|
|
stop updating.
|
|
- **`family` is now load-bearing**, not decoration: `body`, `static` and `land` rows share the table
|
|
and have different lifetimes. The admin status counts them separately for the same reason — there
|
|
is no "how many are there" to compare `static` against, so the only honest number is how many the
|
|
site has been asked for and holds.
|
|
|
|
Pictures land in `server/uploads/items/` (gitignored like `uploads/atlas/`), content-addressed the
|
|
same way — `uo-static-3922-h33-<sha8>.png`. A key the shard has no art for writes **no row at all**:
|
|
an empty row would make it "held" and it would never be asked again, including after the operator
|
|
patches in the graphic that was missing.
|
|
|
|
## shard_clilocs / shard_cliloc_meta — UO's localization table (Protocol 3.0)
|
|
|
|
Items on the wire carry a `LabelNumber`, not a name. The bridge has always sent it —
|
|
`char.profile.equipment.cliloc`, reward titles as a cliloc number in string form, and one per
|
|
marketplace listing — but with no table to resolve it against, the character sheet could only render
|
|
`id 1023721` where the game renders "quarter staff".
|
|
|
|
| Table | Shape |
|
|
|---|---|
|
|
| `shard_clilocs` | `number` INT PK, `flag`, `text` TEXT |
|
|
| `shard_cliloc_meta` | Singleton (`id = 1`), `payload` JSON (`source` — `bridge` or `file` — the base's fingerprint under `base`, the overlay `hashes`, per-source counts, `parserVersion`), `imported_at` |
|
|
|
|
Import-owned and all-or-nothing in one transaction, same contract as the atlas — including **`DELETE`,
|
|
not `TRUNCATE`**, for the same reason.
|
|
|
|
**The base table comes from the SHARD** on any install with uo-link configured (Protocol 8, phase 2):
|
|
it reads its own client's compressed `Cliloc.enu` and serves the table paged over the bridge, so
|
|
nothing is converted and nothing is copied to the web host. Without a shard link it falls back to a
|
|
converted file on disk at a path from the `cliloc_client_path` setting, then `UO_CLIENT_PATH` — the
|
|
pre-protocol-8 pipeline, deprecated rather than removed so an existing install keeps working.
|
|
|
|
**Overlays are always the filesystem's**, either way: ServUO has no server-side notion of a custom
|
|
cliloc, so `custom/` is the only place shard-added ids exist and there is nothing on the shard to ask
|
|
for. That gap is in the *game*, not in this pipeline.
|
|
|
|
Nothing client-derived is committed: UO's strings are EA's, exactly as the creature sprites are. A
|
|
shard with nothing configured is fully supported — names render as ids. Full design and operator
|
|
guide: [`CLILOCS.md`](../../website/CLILOCS.md).
|
|
|
|
**It reads a SET of sources, not one file**, because shards edit items and add new ones and those
|
|
carry cliloc ids no stock client table has. A base (from the shard, or a converted file) plus every
|
|
overlay under `custom/` are hash-gated **together**, exactly as the atlas re-reads
|
|
`Regions.xml` + `Locations/*.xml` + `Spawns/*.xml` + `ChampionSpawns.xml`. Later sources win, so an
|
|
overlay both adds ids and overrides stock ones, and adding one custom item never means re-exporting a
|
|
5 MB client file. Scale, measured on the live shard: its script tree references 16,434 cliloc ids and
|
|
only 37 are absent from stock — tens of entries against a 67k base, which is why this is an overlay
|
|
and not a second table.
|
|
|
|
**The conversion step used to be unavoidable, and is not any more.** Every current client ships its
|
|
cliloc files compressed (first DWORD's high byte `0x8E`) and ServUO's own bundled
|
|
`Ultima.StringList` cannot read that either — which is why, for two protocol versions, the operator
|
|
had to install UOFiddler, build a converter against its `Ultima.dll` and copy a 5 MB file to the web
|
|
host. Protocol 8 phase 2 ported the Mythic decompressor into the overlay, so **the shard reads its
|
|
own client and supplies the names**. The file half survives only as the fallback above, where the
|
|
plain layout and a delimited text export are both accepted, sniffed by header rather than extension.
|
|
|
|
Two consequences for what this table holds. `shard_cliloc_meta.payload` carries the base's
|
|
fingerprint under `meta.base` (the shard's file size, mtime, hash and `EXTRACTOR_VERSION`) separately
|
|
from the overlay hashes, because on the bridge the old `clilocs.plain` label is *supposed* to
|
|
disappear and a single hash map would read that upgrade as a vanished source. And **boot does not
|
|
import on the bridge path**: a file could be re-hashed locally on every restart, but asking the shard
|
|
would put a sidecar round trip in the boot sequence for a table that changes only when an operator
|
|
patches their client. Importing is an admin action.
|
|
|
|
Three decisions worth stating:
|
|
|
|
- **`text` is TEXT, not VARCHAR.** Long property descriptions reach 12 KB. The index that matters for
|
|
marketplace search is the denormalized `shard_vendor_items.display_name`, not this table.
|
|
- **Blank entries are dropped at import** — 123,490 parsed → **67,496** stored. Roughly half a cliloc
|
|
table is empty strings for ids the client reserves and never uses; a row that resolves to no name is
|
|
indistinguishable from no row at all, and dropping them makes the binary and text imports converge
|
|
on identical content.
|
|
- **Two refusals, one of them the atlas's.** A corrupt source fails the parse on a truncated record,
|
|
so it is caught outright and leaves the previous table serving. But a source that has **vanished**
|
|
parses perfectly and imports a table quietly missing everything it contributed — an unmounted volume
|
|
and a deliberate deletion are indistinguishable from here, which is precisely the ambiguity the
|
|
atlas stages a facet removal for. So it is escalated: `status: 'needsReview'`, nothing applied,
|
|
`missingSources` reported by both the import and `status()`, and an admin accepts it with
|
|
`{approve:true}`. That is a flag rather than the atlas's approve/reject pair because the atlas
|
|
stores a pending decision so that approving **re-parses** the tree; here nothing is stored, so
|
|
re-reading at approval time is automatic.
|
|
|
|
**Resolution is server-side and there is no public route.** The table is never served *as* a table:
|
|
67k rows would dwarf any page using them, and the Android client consumes the same already-resolved
|
|
JSON. `resolveMany()` returns only ids that resolved to something displayable — placeholders like
|
|
`~1_val~` are stripped, since the bridge sends the id and never the property packet that carries the
|
|
arguments — and it never throws, because a cliloc lookup is decoration on a character sheet.
|
|
|