Season cutover runbook

The cross-repo procedure for rotating AFK Journey's seasonal content (seasonal artifacts, phantimals, charms) across afkj-parser, chaldea and stargazer.

Contents

  1. In short
  2. Repos and roles
  3. Phase 1: structural cutover
  4. Phase 2: text
  5. Phase 3: targeting unlock
  6. Phase 4: retire the previous season
  7. Gotchas seen so far

Stargazer's own contract (file layout, lints, retirement rules) lives in ../stargazer/docs/architecture/SEASONAL.md and is referenced here, not copied; this page adds the repo names, commands and ordering that Stargazer's public docs deliberately leave out. What each phase takes from this repo's extract and feed is in Integrations §2.2.

The expected shape of every cutover is structural first, text later: the game flips seasons before the upstream text feed carries the new season's skill text, so ids, names, stats and portraits ship at the flip and the text follows once the feed catches up. Seasonal targeting (Spirit Marks, artifact arrows) unlocks later still.

1  In short

Four phases, each shipped on its own. Rough sizes are for a typical season (12 artifacts, 5 phantimals).

Phase 1, at the season flip. Most of the work; mostly mechanical.

  1. Close the game; run the four extractor commands (artifact icons, phantimal portraits, artifact stats, locales).
  2. python3 tools/seasonal_icons.py here, push chaldea, confirm every icon URL returns 200.
  3. Stargazer:
    • retire the old season: three --retire importer runs plus one git rm;
    • add the new season: 12 artifact files (copied as-is), 5 phantimal files (hand-written: id, range; faction from the game data, never guessed), 17 name files (en/zh from the locales), 5 name-only stubs;
    • replace the seasonal skill file (companions, no Spirit Marks yet) and delete the old artifact rules;
    • bump the season pin, run the gates, deploy.

Phase 2, when the text feed catches up. Small: rebuild the feed, npm run import:seasonal, fix any data file a lint flags, deploy.

Phase 3, targeting, per unit. Tiny each time: an artifact rule is on as soon as it's added; a phantimal's Spirit Mark is written any time, then switched on with "targeting": true when it unlocks in-game.

Phase 4, after the phase 1 deploy. Tiny: git rm the old season's icons in chaldea, and delete its calibration samples and rounds here, screenshots included.

2  Repos and roles

RepoRole
afkj-parser (this repo)Extracts from the local game install (artifact icons and stats, phantimal portraits with companions, locale strings) and builds the feed Stargazer's importers read (out/api/). Converts the season's icons for the image host (tools/seasonal_icons.py) and holds the match-screenshot import's calibration (calibration/).
chaldeaImage host: publishes dist/ on push. Stargazer fetches https://chaldea.tmdict.com/img/seasonal/{artifact,phantimal}/<slug>.webp.
AFKJourneyDataGame-data dump, committed per game build; afkj-parser's fallback source when an extract fails.
stargazerConsumer. Hand-curates ids, names, ranges, targeting; imports text.
PhaseWhenReposResult
1. Structuralthe game flips seasonsafkj-parser, chaldea, stargazernew units placeable with portraits, names and stats; text shows "Skill details not yet available"
2. Textthe feed carries the seasonafkj-parser, stargazerskill, effect and charm text live
3. Targetingartifact mechanics known; phantimal targeting unlocks in-game (weeks later) stargazerartifact arrows / Spirit Marks drawn
4. Retireafter the phase 1 deploychaldea, afkj-parserthe previous season's icons and calibration screenshots removed

3  Phase 1: structural cutover

3.1 Extract

Close the game first: the extractor refuses to run while it is open. The locales come from the game's CDN, all 17 of them, after the extractor checks that the CDN serves the installed build (if not: open the game, let it update, close it, re-run).

cd afkj-parser
./afkj-extract --locales-from cdn      # tables + all 17 languages
./afkj-parse build                     # the feed in out/api/; ends with PASSED, diff in out/build/diff.md
./afkj-extract --only artifacts --only phantimals   # out/images/{artifacts,phantimals}/<slug>.png + manifest.csv

The feed carries everything Stargazer's structural files need (ids, stats, factions, ranges, en/zh names); Stargazer writes them in 3.3. In out/images/phantimals/manifest.csv, role is main for the phantimal and sub for a companion, which belongs to the main of the same camp (3.3, step 4).

3.2 Icons

cd afkj-parser
python3 tools/seasonal_icons.py          # writes ../chaldea/dist/img/seasonal/{artifact,phantimal}/<slug>.{png,webp,avif}
cd ../chaldea
git add dist/img/seasonal && git commit -m "season N icons" && git push

The host publishes dist/ on push. Confirm every slug before Stargazer deploys, or new units render as broken images on first load. From chaldea:

for f in dist/img/seasonal/{artifact,phantimal}/*.webp; do
  p=${f#dist/}
  printf '%s %s\n' "$(curl -sI "https://chaldea.tmdict.com/$p" | head -1 | awk '{print $2}')" "$p"
done | grep -v '^200'   # prints nothing when all are live

chaldea's dist/_headers already sends Access-Control-Allow-Origin: * on /img/*, which the canvas image export needs.

3.3 Stargazer

Follow SEASONAL.md, "Season cutover", phase 1. In file terms:

  1. npm run import:phantimals -- --retire, npm run import:artifacts -- --retire, npm run import:charms -- --retire.
  2. npm run import:structure: writes the season's artifact and phantimal data files and the artifact names from the feed (ids, stats, factions, ranges, both factions for the phantimal that counts for two), removes the outgoing season's, and keeps hand-set fields such as targeting. It stops on an id that would change within a season.
  3. A new stat (e.g. crit in S8): here, its game code and stargazer key in feed/contract.py STAT_KEYS (until then the build fails F15) and a rebuild; in Stargazer, an ArtifactStatKey + PERCENT_STAT_KEYS decision (src/lib/types/artifact.ts) and src/locales/game/<key>.json; then re-run step 2.
  4. src/lib/skills/seasonal/phantimal.ts: empty spiritMarks, one createCompanionSkill({ raisesCapacity: false, companionImageModifier: '<companion slug>', companionRange }) per phantimal with a sub row. Its season-specific cases go in the season N block of tests/unit/skills/phantimal.test.ts.
  5. Delete the outgoing season's ARTIFACT_TARGETING entries and test cases.
  6. Bump the pin in tests/unit/lib/seasonal.test.ts.
  7. Maps / Supreme League row if the list changed (SEASONAL.md).
  8. npm run lint, npm run type-check, npx vitest run, npm run build; try a link with the new phantimal and its companion; commit; deploy.

Done when the data-contract tests in tests/unit/lib/seasonal.test.ts pass (they fail on a forgotten pin, a skill file or artifact rule left from the previous season), every icon URL returns 200, and the new units' modals show "Skill details not yet available".

4  Phase 2: text

  1. Here, with the game closed: ./afkj-extract --locales-from cdn, then ./afkj-parse build. It ends with PASSED or FAILED, and out/build/diff.md lists what changed since the last import (the new season's artifacts, phantimals and charms should be there). If the extract fails, ./afkj-parse build --source ../AFKJourneyData builds from the dump. A glossary keyword the new text uses for the first time fails F15 until it has a name in feed/contract.py KEYWORD_NAMES: the English word players read, in lowercase with hyphens.
  2. In Stargazer: npm run import:seasonal. The phantimal stubs are replaced by full locale files, charm text returns, and every lint checks phase 1's hand-written files against the feed (slug sets, factions, ids, seasons, stats). A failure here means a phase 1 file is wrong; fix the file, never the feed output.
  3. In Stargazer, once the charm text is in: write the season's src/data/seasonal/charm/tags.json against the new ids (s9-1 …). out/internal/keys.json says which charm each id is, and the Debuff, Haste and Temp Buff tools read the tags through it.
  4. Review the diff (names should match phase 1's zh lookups), run the gates, deploy. The "Skill details not yet available" line disappears on its own.
  5. Here: ./afkj-parse accept, so the next update is compared with this feed.

Done when no file in src/locales/seasonal/phantimal/ still has "skills": [], every seasonal artifact has a file in src/locales/seasonal/artifact/effects/, and tests/unit/charms.test.ts runs its per-charm checks instead of skipping them. Artifacts, phantimals and charms can land in the feed on different builds; repeat the phase for each.

5  Phase 3: targeting unlock

6  Phase 4: retire the previous season

After the phase 1 deploy nothing requests the old slugs (reused ids resolve to the new season's content; stale saved teams show an "S{n}" placeholder), and a retired season never returns. Delete its artifact and phantimal icons in chaldea, and its calibration screenshots and rounds here: they show its artifacts, and the new season's calibration starts from fresh screenshots. Git history keeps the icons and the rounds' records; the screenshots are out of git and go for good. Hero portraits, costume references, frames, star rows, Stargazer's bundled hero descriptors and the permanent artifacts carry over untouched, and so does calibration/replay/: Supreme League maps change on their own schedule, and its README covers a map change. calibration/roster/ stays too: the roster screen shows heroes, not a season's artifacts.

cd chaldea
git rm dist/img/seasonal/artifact/<old-slug>.* dist/img/seasonal/phantimal/<old-slug>.*
git commit -m "retire season N" && git push
cd ../afkj-parser
git rm -r calibration/match/samples calibration/match/rounds
rm -rf calibration/match/samples calibration/match/rounds   # the screenshots and pages git ignores
git commit -m "retire season N calibration"

The live slugs are exactly Stargazer's src/data/seasonal/{artifact,phantimal}/*.json names plus each companion's companionImageModifier; anything else under chaldea's dist/img/seasonal/ can be deleted. Until the old calibration is removed, npm run check in calibration/ stops on its artifact labels.

7  Gotchas seen so far