Skip to content

Release: develop → main - #145

Open
jfberry wants to merge 216 commits into
mainfrom
develop
Open

Release: develop → main#145
jfberry wants to merge 216 commits into
mainfrom
develop

Conversation

@jfberry

@jfberry jfberry commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Long-lived release PR for the next development cycle (successor to #121, which shipped 5.1.0). Version on this branch: 5.2.0.

Accumulated so far

OpenAPI-documented API + strict /api/v2 (huma-api-migration)

  • One huma instance on the authenticated /api group: live OpenAPI 3.1 spec at GET /openapi.json, interactive docs at GET /docs, RFC 9457 problem+json errors across the documented surface.
  • ~52 existing endpoints documented in place (same paths, same success JSON): reloads, weather/stats/geocode, geofence reads + tile URLs, masterdata, snapshots, DTS editor, config editor, autocreate, summaries, command/test/deliverMessages/resolve.
  • New strict /api/v2: 11 tracking types (including the new incident type) on human-scoped paths /api/v2/humans/{id}/tracking/{type} with GET/POST/PUT/DELETE + bulk delete + full snapshot; discrete typed humans/profiles/locations/roles actions; new PUT …/locations/{label}; omit-to-wildcard requests with null-at-wildcard responses; pvp_ranking_evolution exposed.
  • v1 tracking/humans/profiles remain frozen on gin, deprecated-but-supported. Migration docs: docs/v1-to-v2-migration-guide.md, design in docs/v2-api-design.md, RFC RFC: PoracleNG v2 API — clean, documented, OpenAPI-first (feedback wanted) #138.
  • Golden OpenAPI spec tests pin the whole surface; CHANGELOG.md started.

v2 mutes API (#146)

  • GET/POST /api/v2/humans/{id}/mutes and DELETE …/mutes[?scope=&value=] expose the in-memory alert mutes (previously bot-command/button-only) with bot-parity validation; the v2 full snapshot now carries a mutes array. Mutes stay volatile by design (cleared on restart, documented on the wire). Design: docs/superpowers/specs/2026-06-11-mute-api-design.md.

Bug fixes

Dependencies & housekeeping

Every merge to this branch passed the full local gate (build / vet / test / golangci-lint).

🤖 Generated with Claude Code

dependabot Bot and others added 30 commits May 29, 2026 18:28
Bumps the minor-and-patch group in /processor with 1 update: [github.com/go-telegram/bot](https://github.com/go-telegram/bot).


Updates `github.com/go-telegram/bot` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/go-telegram/bot/releases)
- [Changelog](https://github.com/go-telegram/bot/blob/main/CHANGELOG.md)
- [Commits](go-telegram/bot@v1.20.0...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/go-telegram/bot
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Design spec for migrating the tracking/humans/profiles endpoint groups
from hand-written Gin handlers to huma, for OpenAPI docs discoverability
and a type-honest API while preserving the legacy wire envelope and
lenient client tolerance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mutating the registry's stored *Schema for T contaminated strict handlers
for the same type. Shallow-copy before flipping AdditionalProperties. Add a
regression test proving a strict endpoint still 422s on unknown fields after
a lenient registration of the same type, and harden the leniency test to
assert decoded values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… error message

huma's default schema-link transformer injected a $schema field into every
response body, breaking byte-compatibility of the legacy {status,...} envelope
on both success and error responses. Drop the transformer. Also fold huma's
per-field validation details into the legacy error message so 422s are no
longer the opaque "validation failed".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o server

Wires NewHumaAPI into main.go (serving public /openapi.json and /docs) and
moves the pokemon-list endpoint from gin to huma as the worked-example
template. Legacy {status:ok,pokemon:[...]} envelope preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y decomposition

Single-object-or-array body, lenient item schemas (additionalProperties), and
decomposition of the clean bitmask into caller-facing clean/edit/summary
booleans (legacy integer clean still accepted), collapsed to the packed column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ented defaults, debug logging

profile_no/silent/suppressMessage modeled as optional (no -1/string-presence
magic in the spec); body field server-defaults documented and pokemon_id marked
required; restored debug body logging dropped in the gin->huma move; shared test
helper + dead-var cleanup. Template hardened before the 9-type fan-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssage as bool

profile_no now appears as type:integer in the spec (omit or 0 = active profile;
profiles are 1-indexed). silent/suppressMessage are proper optional booleans
instead of string-presence flags. Applied to GET and POST pokemon tracking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…udit in real types

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inconsistencies)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er/league

Reusable enum field types: canonical string enum in the OpenAPI, legacy integer
(and numeric string) still accepted. Applied to pokemon gender and
pvp_ranking_league. Shared toolkit for the tracking fan-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecation stance, quest/nest/invasion fields)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ight from pokemon

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-mega-evolution PR

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es v1 silent+suppressMessage)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…i endpoints

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tance)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CRUD (+new PUT)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt + frozen v1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… full snapshot endpoint

Restore v1's per-user scoping (DeleteByUID is WHERE id AND uid). Path-scope rules under /api/v2/humans/{id}/tracking/{type}. Add GET /api/v2/humans/{id}/tracking full snapshot (human + tracking-by-type + profiles + locations + summaries; ?all_profiles, ?include_descriptions). Profiles become a human sub-resource.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jfberry and others added 30 commits July 18, 2026 21:38
… live path)

Review Minor: processTestWeatherChange passed AlertType "weather"; every other
derived type mirrors its live AlertType, and weather.go uses "weatherchange".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uilder)

Adds the "questSummary" derived DTS type to the enrich/test-command
unification work (superpowers/sdd task 5): a testdata.json partial
({reward:{type,reward,form}, quests:[...]}) drives enrichQuestSummary,
which re-enriches each quest via questEnrichOne and renders the group
through a new buildQuestSummaryGroupView helper factored out of the live
DispatchQuestSummary scheduler, so both paths build identical output.
questEnrichOne also now sets withAR itself (from the parsed webhook)
instead of requiring the caller to copy it from a buffered row, letting
the derived test path (with no buffered row) get it for free.

Wires enrichForType, EnrichWebhook, ProcessTest, and !poracle-test
(as "quest-summary", wire spelling "quest_summary" — mirroring the
fort-update/max-battle convention, since the command parser lowercases
args before matching validHooks, so a camelCase entry would never match).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the "monsterChanged" derived DTS test type end-to-end (superpowers/sdd
task 6): enrichMonsterChanged parses an {old,new} pokemon-webhook partial,
enriches `new` via the existing enrichPokemon path, and builds the
{{original.X}} prior-sighting view from `old` via the new
tracker.EncounterStateFromPokemon helper + dts.BuildOriginalView (the same
builder dispatchPokemonAlert's live change path falls back to).

- internal/tracker/encounter.go: factor the webhook->EncounterState mapping
  out of ProcessPokemon into EncounterStateFromPokemon so both the live
  pokemon handler and the new test builder share one implementation.
- cmd/processor/enrich.go: enrichMonsterChanged + enrichForType dispatch +
  injectOriginalExtra (exposes original.* on the /api/dts/enrich flattened
  variable map, which LayeredView.Flatten doesn't cover on its own).
- cmd/processor/test.go: processTestMonsterChanged builds an IsChange=true
  RenderJob with OriginalView/ReplyKey so !poracle-test renders through
  RenderPokemonChanged like a live change notification; wired into
  ProcessTest and resolveDTSTypeFromRaw under the "monster_changed" wire
  type.
- internal/bot/commands/poracletest.go: "monster-changed" CLI hook name,
  resolveDTSType mapping, and disappear_time freshening for the `new` half
  (deep-copied to avoid mutating the shared testdata sample).
- fallbacks/testdata.json: "monster_changed"/"ditto-reveal" sample — an
  unencountered disguised Foongus revealed as an encountered Ditto, the
  real-world scenario dispatchPokemonAlert's ChangeSpecies bucket exists for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch live render)

enrichMonsterChanged enriched `new` and built OriginalView from `old` but
never populated the per-language changeType/changeTypeText fields, so the
bundled monsterChanged templates' ungarded {{changeTypeText}} rendered a
dangling "— " on !poracle-test monster-changed,ditto-reveal. Compute the
species/stats bucket directly from the old/new EncounterState snapshots
(monsterChangedBucket) and reuse the live path's translation-key convention
(extracted into changeTypeText, shared with perLangWithChangeFields) to set
perLang["changeType"]/["changeTypeText"], threaded through to the RenderJob
via the existing renderJobFromEnrich copy. Also relabels the ditto-reveal
sample's location to "keep" since the relocate logic never reaches its
nested old/new lat/lon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leanTTH)

Adds the last derived poracle-test type: rsvpChanges reuses enrichRaid
wholesale (webhook.RaidWebhook already carries the rsvps array natively)
and layers on extras["overrideCleanTTH"], mirroring ProcessRaid's
latestFutureTimeslotSec computation. processTestRsvpChanges wires the
same raidlife:{gymID}:{raidEnd} EditKey/ReplyKey convention the live
rsvpChanges render job uses. Wires enrichForType, ProcessTest, and
poracletest.go (validHooks, resolveDTSType, timeslot freshening) for
the rsvp-changes/rsvp_changes/rsvpChanges spelling chain, plus a new
fallbacks/testdata.json sample and fixes a stale monsterchanged_test.go
assertion that assumed rsvpChanges was still unimplemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /api/dts/testdata now accepts ?dtsType=<name>: resolves the name via
the shared internal/dtsmap alias table and returns only the entries that
preview that DTS type, each tagged with dtsType. Two source webhook types
need a payload-shape split beyond a plain type match (mirroring
resolveDTSTypeFromRaw): pokestop -> invasion/lure (by grunt vs lure
fields) and raid -> raid/egg (by pokemon_id). The response also exposes
the full DTS-type -> source map (webhookType, templateType, derived) so
the editor can drop its hardcoded copy. The legacy ?type=<webhookType>
filter is unchanged and untagged.

Moved the canonical alias table (dtsSource/dtsAlias/dtsTypeMap) out of
package main into internal/dtsmap so internal/api can share it without
duplication; cmd/processor/dts_alias.go now re-exports thin wrappers so
existing call sites and tests are unaffected.

While wiring the table into testdata matching, corrected the four derived
types' WebhookType values (monsterChanged/rsvpChanges/questSummary/
weatherchange), which had drifted to a hyphenated placeholder spelling
that never matched the underscore/no-separator wire spelling testdata.json
and !poracle-test actually use; added identity entries for both the CLI
hyphenated form and the wire form so all spellings keep resolving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… vs real testdata

dtsmap's "fort-update" identity entry self-mapped WebhookType to the
hyphenated "fort-update", which matches no testdata.json entry (the real
type/wire spelling is "fort_update") — ?dtsType=fort-update silently
returned zero results, breaking the DTS editor, which addresses the fort
update template by its TemplateType name "fort-update". Same mismatch
class Task 8 already fixed for the four derived types. Corrected the
entry's WebhookType to "fort_update" so both "fort-update" and
"fort_update" resolve identically; enrichForType's literal
"fort_update"/"fort-update" switch and !poracle-test's dispatch were
already spelling-agnostic and are unaffected.

Added a regression-guard test that loads the REAL fallbacks/testdata.json
and asserts every dtsmap.TypeMap() name resolves to at least one entry
(except the documented nest/greeting exceptions), so a future testdata
"type" rename or WebhookType typo can't silently break ?dtsType=
filtering again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the leading !poracle-test type token through the shared dtsmap
alias table (case-insensitively, via a new AliasFold, since the bot
parser lowercases every token before Run sees it) whenever it isn't
already one of the existing raw wire-type/CLI-hyphenated spellings.
This lets DTS template-type names like "monster", "maxbattle", "egg",
and the lowercased "monsterchanged"/"rsvpchanges"/"questsummary" load
the same testdata entry and dispatch the same wire type as their
existing webhook-type/hyphenated forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server contract for the DTS editor: DTS-name-addressable /api/dts/enrich,
DTS-type-aware /api/dts/testdata (?dtsType + tags + types map), the derived-type
partials, and the exact editor delete-list (hardcoded dtsToWebhookType map,
client-side pokestop filter, monsterNoIv/egg special-casing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iew includes synthetic PVP

Fix 1 (D6 completeness): RegisterTest (internal/api/huma_actions.go) passed
req.Type straight to ProcessTest, whose switch only accepts wire types.
POST /api/test {"type":"monsterChanged",...} 500'd with "unsupported test
webhook type", even though !poracle-test's resolveHookType already resolves
DTS names for the bot path. Add resolveTestWireType (internal/api/test_type.go),
which resolves a DTS template-type name, raw wire type, or hyphenated
CLI-display spelling to ProcessTest's wire type via dtsmap.AliasFold —
case-insensitive, idempotent on wire types. Its pokestop handling
deliberately diverges from resolveHookType: invasion/lure resolve to their
own token rather than collapsing to "pokestop", since ProcessTest's switch
(unlike !poracle-test's testdata.json bucket lookup) already dispatches
"invasion" directly and has no "lure" case at all — collapsing would either
be a no-op or a scope-creeping behavior change. Unknown types pass through
unchanged so ProcessTest's own error still fires. Also added the missing
"max-battle" hyphenated identity entry to dtsmap (mirroring the existing
"fort-update" entry) and updated the `type` field's OpenAPI doc string
(golden regenerated).

Fix 2: EnrichWebhook's synthetic "_editor" PVP display block gated on
`dtsAlias(webhookType).WebhookType == "pokemon"`, which covers
monster/monsterNoIv but misses monsterChanged (WebhookType is the derived
"monster_changed" spelling) even though its enrichment is a pokemon spawn
and the live !poracle-test path always computes this block. Gate on the
alias-authoritative result.templateType instead, so all three pokemon-family
template types compute the block consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: testable derived DTS types + unified enrichment + DTS-name-addressed test data
Mirror stardust (type 3) as an amount-based quest reward: min amount
stored in the Reward column. Covers matcher, enrichment (fields, icon,
rewardString), bot command grammar (pokecoins:N / bare pokecoins /
everything / remove), slash option, NLP keyword, summary grouping,
rowtext, DTS fields, i18n labels, docs, and a poracle-test scenario.
The pokecoins feature was branched from main, which predates the shared
validRewardTypes guard added on develop. Both the v1
(/api/tracking/quest/{id}) and v2 (/api/v2/humans/{id}/tracking/quest)
handlers gate on that map, so reward_type 8 was rejected (400/422): a
pokecoins rule could be created by the bot but not the API, and an
existing bot-created rule aborted API bulk saves on the first invalid row.

- add 8 to validRewardTypes (fixes both v1 and v2)
- document pokecoins in the v2 reward_type / reward field docs
- extend the v2 accept-all test + add a reward=amount round-trip test
- regenerate the OpenAPI golden

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gamelocale already ships quest_reward_8 ("PokéCoin"), which the i18n
bundle loads under the same key. The embedded en.json override only
duplicated it (as "Pokécoins"), so remove it and rely on gamelocale like
every other quest_reward_* label (stardust, candy, mega energy). English
now renders "<amount> PokéCoin" from gamelocale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ported PoracleJS quest templates reference quest_task / quest_reward /
quest_conditions, but the processor only exposes the translated values as
questString / rewardString / conditionString, so {{quest_task}} et al.
resolved to nothing. The value was always computed — this was an alias
gap, not a missing field.

Add the three snake_case aliases to the quest alias table. Aliases resolve
ahead of the raw webhook layer, so the translated objective wins over any
scanner-supplied quest_task (Golbat sends `title`; MAD's quest_task is
obsolete). Documented in DTS.md; covered by a LayeredView alias test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(quest): add pokecoins reward tracking (type 8)
fix(dts): restore legacy PoracleJS quest field aliases
translateMonsterName suppressed any form whose translated name was
"Normal"/"Unset"/"" regardless of the form value. So an explicitly-tracked
Normal form (e.g. Rattata form 45, stored as a non-zero value) rendered
identically to the "any form" tracking (form 0) and to a second Normal
row — the user saw duplicate-looking "Rattata" rows with no way to tell
them apart.

Only form 0 ("any form") should drop a default Normal/Unset label. An
explicit form (form != 0) now keeps its name, including "Normal", so
distinct form trackings are distinguishable. Alola (form 46) was already
shown because "Alola" isn't a Normal-ish name.

Covered by a table-driven translateMonsterName test (normal/named/any).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TestPoracleTest_DTSNameLoadsSameEntryAsWireType loads the same testdata
entry twice (via "monster" and "pokemon" aliases) and compared the two
dispatched payloads byte-for-byte. PoracleTestCommand freshens
disappear_time from time.Now() at each dispatch, so when the two loads
straddled a one-second boundary the payloads differed by 1s and the test
failed — intermittently, on ~1% of CI runs.

Normalize the freshened timestamp fields (disappear_time / start / end)
out of both payloads before comparing, so the test proves "same entry"
deterministically. Added a direct normalizeTestPayload test that encodes
the exact 1-second-delta failure and guards against masking real diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(rowtext): show explicitly-tracked Normal form in !tracked
… from config

!poracle-test weatherchange showed no changed pokemon. The bundled and
operator weatherchange templates iterate {{#each activePokemons}}, but
WeatherTranslate only populates the activePokemons key when
[weather] show_altered_pokemon_static_map is set. enrichWeatherChange
hardcoded that flag false, so the test render never matched production
(where the flag is on) and the affected-pokemon list rendered as the
"could have altered the reported stats" fallback branch instead.

Read the flag from ps.cfg.Weather (nil-safe), exactly like the live
consumeWeatherChanges path, so !poracle-test faithfully reproduces the
live alert. The nil guard preserves the flag-off default for the
enrich-parity test harness (which leaves ps.cfg nil).

Tests: activePokemons is populated when the flag is on (was the failing
repro) and stays absent under a nil cfg (enrichedActivePokemons still
carries the data — no panic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the show_altered_pokemon_static_map fix: with that flag on,
enricher.Weather returns no base tile — it defers to the per-user tile
that WeatherTranslate builds with active-pokemon markers. But
enrichWeatherChange discarded WeatherTranslate's pending (perLang, _ =
...), so once the flag read true the weather-change tile vanished from
the test render entirely.

Capture the per-user pending and prefer it over the (now-nil) base
pending, mirroring the live consumeWeatherChanges selection ("use
per-user tile if available, otherwise base tile").

Test: build a real staticmap.Resolver (tileservercache, an RFC 6761
.test URL so workers fail DNS instantly with no egress) and assert the
returned tilePending is non-nil with the flag both on (per-user tile)
and off (base tile). Reintroducing the discard fails the flag-on arm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h types map

POST /api/dts/enrich {type:"showcase"} returned 500 "unsupported webhook
type: showcase" while GET /api/dts/testdata exposed showcase in both the
types map (webhookType/templateType showcase, non-derived) and the
?dtsType=showcase bucket. showcase is a real, distinct DTS template type
(leaderboard + featured focus) with its own field set, fallback
template, and live handlers (ProcessShowcase / processTestShowcase) — but
the unified enrichForType dispatch that backs /api/dts/enrich never got a
showcase case, so the enrich side and the testdata/types side disagreed.

Add enrichShowcase (parses webhook.ShowcaseWebhook, enriches via
enricher.Invasion + InvasionTranslate over the rankings +
ShowcaseFocusTranslate, templateType "showcase") and wire it into
enrichForType's non-derived switch. Refactor processTestShowcase onto the
same enrichShowcase + renderJobFromEnrich core so the editor-enrich and
live-test paths share one implementation (the third copy this dispatch
was meant to eliminate) — the enqueued RenderJob still carries AlertType
"incident" and TemplateType "showcase", unchanged.

Tests: enrichForType("showcase") and EnrichWebhook("showcase") now
enrich (was 500); processTestShowcase still enqueues incident/showcase
with the leaderboard data in per-language enrichment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…time

test(poracletest): de-flake DTSName payload comparison
fix(dts): showcase enrich (#172) + weatherchange enrich follow-ups
Adds a `derivedDtsTypes` boolean to the GET /health capabilities map,
alongside the existing flags (buttons, snapshots, autocreate, tomlDts,
buttonResponseObject). It is hardcoded true here because this binary
carries the enhanced DTS enricher — the one that returns the DTS-type
`types` map from GET /api/dts/testdata, honours ?dtsType=<name>, and
accepts DTS type names (including derived types: monsterChanged,
incident, questSummary, weatherchange, rsvpChanges — and showcase) at
POST /api/dts/enrich.

Those pieces ship together, so a single flag represents the whole
feature: "flag present and true" is exactly equivalent to "enricher
available". Non-breaking — editors that don't see the key fall back to
structural detection (presence of the `types` map); the flag just lets
them detect support from the unauthenticated /health response and skip
an extra round-trip. No other endpoint or response shape changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(health): add derivedDtsTypes capability flag
…dow the fallback

Saving an override of a platform-agnostic fallback (the per-command help
templates, loaded from fallbacks/dts/help/*.json with platform="")
produced a DUPLICATE in the editor's template list: the readonly
fallback (help, fort, "") plus the user's copy (help, fort, "discord").

Root cause: POST /api/dts/templates rejected platform="" (400 "missing
required fields"), forcing the editor to save help with a concrete
platform. That platform-specific entry has a different entryKey than the
agnostic fallback (entryKey includes platform), so the dedup — which
drops a readonly fallback only when a user entry shares its (type,
platform) surface — never dropped it. Both surfaced.

Fix (server half of the coordinated fix): allow platform="" on save for
platform-agnostic types via dts.IsPlatformAgnosticType (help today).
A saved (help, fort, "") override then shares the fallback's key and
cleanly shadows it — one entry, not two. Non-agnostic types (monster,
raid, …) still require a concrete platform. Also tidy entryFilename so an
empty platform yields "help-fort.json" rather than "help-fort-.json".

The editor half (poracle-embed-visualizer) must stop coercing empty
platform to "discord" (useDts.js: `e.platform || 'discord'`) for agnostic
types so it sends platform="".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Companion to the server fix in this PR: the editor half (stop coercing
empty platform to discord for help; show agnostic entries in every
platform tab; save help with platform=""; surface readonly fallbacks in
the list with a badge). Notes that help is the only agnostic type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(dts): allow platform-agnostic (help) overrides so they shadow the fallback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants