Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@ All notable changes to KiouForge are documented here.

## [Unreleased]

### Added

- **KIOU 1.1.0 support** (CFBundleVersion 15). All 17 hook sites re-verified against the 1.1.0 Mach-O; every struct offset the KIF pipeline reads is unchanged from 1.0.2.

### Changed

- `TARGET_VERSION` now defaults to `1.1.0`; `1.0.1` and `1.0.2` remain buildable by passing it explicitly. The Makefile maps the version to KIOU-Hook's `KIOU_HOOK_TARGET_BUILD` so the dylib and the recipe always agree on which addresses to bake in, and errors out on an unknown version instead of silently falling back.
- The five direct-call RVAs the KIF pipeline uses (`GetUSIText`, `ToSFEN`, `ParseUSI`, `KIFWriteOptions..ctor`, `KIFWriter.Write`) now resolve through the KIOU-Hook catalog instead of being pinned in `Kif/Helpers.m`, so they follow `TARGET_VERSION` like every other site.
- `make deploy` accepts `THEOS_DEVICE_PORT`, needed when the device is reached through an iproxy forward on the host — there port 22 answers as the host rather than the phone.
- FPS override logging only reports transitions. KIOU re-applies its own target every ~5 s, and logging each one buried every other line (198 of 242 lines in one session).
- Bumped the `vendor/KIOU-Hook` submodule to `8ab5868` (per-version RVA headers under `rva/`, `recipes/v1_1_0.py`) and `shared` (Kanade) to develop, which carries the verify_sites overload resolution and the `caves.py` placeholder rows 1.1.0 needs.
- `make deploy` now works on TrollStore Lite devices. `TROLLSTORE_HELPER` defaults to empty and the target SSH-discovers the helper binary at deploy time, preferring `TrollStoreLite.app` over `TrollStore.app` and filtering out stale `TrollStorePersistenceHelper.app` leftovers whose entitlements expire between sessions (which manifest as ssh 255 / SIGKILL 137). The IPA is staged in `/var/mobile/Documents/` instead of `/tmp/` — the trollstorehelper sandbox cannot read `/tmp/` and rejects an IPA there with error 166. The staged file is `chown mobile:mobile`-ed after `scp` so the helper can open it. Non-Lite setups keep working unchanged; operators who pinned `TROLLSTORE_HELPER` in their `.env` are unaffected. `REMOTE_STAGING_DIR` is exposed as an override for setups that stage elsewhere.

## [0.2.1] — 2026-06-26
Expand Down
35 changes: 28 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,20 @@ TARGET_PROCESS := KIOU
TARGET_BUNDLE_ID := com.neconome.shogi

# Override on the command line: make ipa TARGET_VERSION=1.0.2
TARGET_VERSION ?= 1.0.2
TARGET_VERSION ?= 1.1.0
DECRYPTED_IPA ?= $(CURDIR)/assets/$(TARGET_VERSION)/Kiou-$(TARGET_VERSION).ipa

# KIOU-Hook selects its per-version RVA header (vendor/KIOU-Hook/rva/) by
# CFBundleVersion, not by marketing version, so map one to the other here.
# Without the -D below the catalog silently falls back to its own default
# and the dylib gets built against another build's addresses.
KIOU_BUILD_1.0.1 := 11
KIOU_BUILD_1.0.2 := 12
KIOU_BUILD_1.1.0 := 15
KIOU_HOOK_TARGET_BUILD := $(KIOU_BUILD_$(TARGET_VERSION))
ifeq ($(KIOU_HOOK_TARGET_BUILD),)
$(error unknown TARGET_VERSION '$(TARGET_VERSION)'; known: 1.0.1 1.0.2 1.1.0)
endif
IPA_RECIPE := recipes.__init__
KIOU_HOOK_DIR := $(CURDIR)/vendor/KIOU-Hook
IPA_FRAMEWORK := UnityFramework
Expand Down Expand Up @@ -70,6 +82,7 @@ endif
$(TWEAK_NAME)_CFLAGS := -fobjc-arc -Wno-unused-function \
-D$(BUILD_COMMIT_DEFINE)=\"$(BUILD_COMMIT)\" \
-DKIOU_FORGE_VERSION=\"$(PACKAGE_VERSION)\" \
-DKIOU_HOOK_TARGET_BUILD=$(KIOU_HOOK_TARGET_BUILD) \
-ISources/Chinlan -I$(TWEAK_SOURCES_DIR) \
-Ivendor/KIOU-Hook
ifdef FINAL_RELEASE
Expand Down Expand Up @@ -198,18 +211,26 @@ ipa:: chinlan
# INSTALLED_IPA_BUNDLE_ID — bundle id used to relaunch the app
# DEVICE_USER — SSH user (defaults to root)
# REMOTE_STAGING_DIR — sandbox-visible dir to scp the IPA into
# THEOS_DEVICE_PORT — SSH port. Needed when the device is reached
# through an iproxy forward on the host
# (THEOS_DEVICE_IP=host.docker.internal), where
# port 22 answers as the host, not the phone.
# ---------------------------------------------------------------------------
TROLLSTORE_HELPER ?=
INSTALLED_IPA_BUNDLE_ID ?= $(TARGET_BUNDLE_ID)$(if $(BUNDLE_ID_SUFFIX),.$(BUNDLE_ID_SUFFIX),)
DEVICE_USER ?= root
REMOTE_STAGING_DIR ?= /var/mobile/Documents
THEOS_DEVICE_PORT ?= 22
# scp spells the port -P, ssh spells it -p.
DEVICE_SCP := scp -P $(THEOS_DEVICE_PORT)
DEVICE_SSH := ssh -p $(THEOS_DEVICE_PORT)

.PHONY: deploy
deploy: ipa
@helper='$(TROLLSTORE_HELPER)'; \
if [ -z "$$helper" ]; then \
echo "==> discovering trollstorehelper on $(THEOS_DEVICE_IP)"; \
helper=$$(ssh $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
helper=$$($(DEVICE_SSH) $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
"find /var/jb/Applications /var/containers/Bundle/Application \
-maxdepth 4 -type f -name trollstorehelper 2>/dev/null \
| grep -v 'PersistenceHelper' \
Expand All @@ -222,15 +243,15 @@ deploy: ipa
exit 1; \
fi; \
echo "==> helper: $$helper"; \
echo "==> scp $(notdir $(IPA_OUT)) -> $(DEVICE_USER)@$(THEOS_DEVICE_IP):$(REMOTE_STAGING_DIR)/"; \
scp -q $(IPA_OUT) $(DEVICE_USER)@$(THEOS_DEVICE_IP):$(REMOTE_STAGING_DIR)/$(notdir $(IPA_OUT)); \
ssh $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
echo "==> scp $(notdir $(IPA_OUT)) -> $(DEVICE_USER)@$(THEOS_DEVICE_IP):$(THEOS_DEVICE_PORT):$(REMOTE_STAGING_DIR)/"; \
$(DEVICE_SCP) -q $(IPA_OUT) $(DEVICE_USER)@$(THEOS_DEVICE_IP):$(REMOTE_STAGING_DIR)/$(notdir $(IPA_OUT)); \
$(DEVICE_SSH) $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
"chown mobile:mobile '$(REMOTE_STAGING_DIR)/$(notdir $(IPA_OUT))' 2>/dev/null || true"; \
echo "==> trollstorehelper install force $(REMOTE_STAGING_DIR)/$(notdir $(IPA_OUT))"; \
ssh $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
$(DEVICE_SSH) $(DEVICE_USER)@$(THEOS_DEVICE_IP) \
"$$helper install force $(REMOTE_STAGING_DIR)/$(notdir $(IPA_OUT))"
@echo "==> launching $(TARGET_PROCESS) ($(INSTALLED_IPA_BUNDLE_ID))"
@ssh $(DEVICE_USER)@$(THEOS_DEVICE_IP) 'sleep 1; (open $(INSTALLED_IPA_BUNDLE_ID) 2>/dev/null \
@$(DEVICE_SSH) $(DEVICE_USER)@$(THEOS_DEVICE_IP) 'sleep 1; (open $(INSTALLED_IPA_BUNDLE_ID) 2>/dev/null \
|| uiopen $(INSTALLED_IPA_BUNDLE_ID):// 2>/dev/null \
|| echo "no launcher tool; start $(TARGET_PROCESS) manually")'

Expand Down
34 changes: 17 additions & 17 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<p align="center">
<img alt="version" src="https://img.shields.io/badge/version-v0.2.0-2f80ed?style=flat-square" />
<img alt="targets KIOU" src="https://img.shields.io/badge/targets-KIOU%201.0.1%E2%80%931.0.2-ff66a3?style=flat-square" />
<img alt="targets KIOU" src="https://img.shields.io/badge/targets-KIOU%201.0.1%E2%80%931.1.0-ff66a3?style=flat-square" />
<img alt="platform" src="https://img.shields.io/badge/platform-iOS%2013.0%2B-blue?style=flat-square" />
<img alt="arch" src="https://img.shields.io/badge/arch-arm64%20rootless-555?style=flat-square" />
<img alt="runs" src="https://img.shields.io/badge/runs-client--side%20only-1f9d55?style=flat-square" />
Expand Down Expand Up @@ -136,21 +136,21 @@ KIOU はオンラインゲームのため頻繁にアップデートされ、古

### 機能 × バージョン対応表

| 機能 | 1.0.1 (build 11) | 1.0.2 (build 12) |
|---|:---:|:---:|
| FPS Override | ✓ | ✓ |
| AFK Guard | ✓ | ✓ |
| Analysis Tune | ✓ | ✓ |
| Kifu Autosave | ✓ | ✓ |
| **アカウント切り替え** | — | ✓ |
| 機能 | 1.0.1 (build 11) | 1.0.2 (build 12) | 1.1.0 (build 15) |
|---|:---:|:---:|:---:|
| FPS Override | ✓ | ✓ | ✓ |
| AFK Guard | ✓ | ✓ | ✓ |
| Analysis Tune | ✓ | ✓ | ✓ |
| Kifu Autosave | ✓ | ✓ | ✓ |
| **アカウント切り替え** | — | ✓ | ✓ |

アカウント切り替えは 1.0.2 で追加されたフックサイトが必要なため、1.0.1 では非対応です。Jailed / JB ビルドは RVA がコンパイル時に固定されるため、常に**最新バージョンのみ**を対象とします。Patched IPA は `TARGET_VERSION` でビルド時にレシピを選択します
アカウント切り替えは 1.0.2 で追加されたフックサイトが必要なため、1.0.1 では非対応です。アドレスはどの配布形式でもビルド時に `TARGET_VERSION` で決まります。Patched IPA はレシピを、dylib は対応する RVA ヘッダを選択します

### プラットフォーム

| | |
|---|---|
| **最新対応 KIOU** | `1.0.2`(CFBundleVersion 12) |
| **最新対応 KIOU** | `1.1.0`(CFBundleVersion 15) |
| **KIOU の最低 iOS バージョン** | 10.0(アプリの `MinimumOSVersion`) |
| **KiouForge の最低 iOS バージョン** | 13.0(`UIWindowScene` が必要) |
| **動作確認済み** | iOS 15.0 〜 26、arm64 |
Expand Down Expand Up @@ -187,22 +187,22 @@ TrollStore が使えないデバイス向けです。[Sideloadly](https://sidelo
**復号済み** の KIOU IPA が必要です([palera1n](https://palera.in/) + Filza または [TrollDecrypt](https://github.com/donato-fiore/TrollDecrypt) で取得できます)。App Store からダウンロードした IPA は FairPlay で暗号化されているため、そのままでは使用できません。

```sh
# デフォルト(1.0.2
mkdir -p assets/1.0.2
cp ~/Downloads/Kiou-1.0.2.ipa assets/1.0.2/
# デフォルト(1.1.0
mkdir -p assets/1.1.0
cp ~/Downloads/Kiou-1.1.0.ipa assets/1.1.0/
make ipa
# -> packages/ipa/KiouForge-patched.ipa

# バージョンを指定する場合
make ipa TARGET_VERSION=1.0.1
make ipa TARGET_VERSION=1.0.2
```

フックサイトを編集した後や KIOU のアップデート後にビルドする場合:

```sh
# 特定バージョンの dump + IPA でレシピを検証
PYTHONPATH=shared:. TARGET_VERSION=1.0.2 python3 -m tools.verify_sites \
PYTHONPATH=shared:vendor/KIOU-Hook TARGET_VERSION=1.1.0 python3 -m tools.verify_sites \
--recipe recipes \
--index assets/1.0.2/dump.cs.index.json \
--ipa assets/1.0.2/Kiou-1.0.2.ipa
--index assets/1.1.0/dump.cs.index.json \
--ipa assets/1.1.0/Kiou-1.1.0.ipa
```
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<p align="center">
<img alt="version" src="https://img.shields.io/badge/version-v0.2.0-2f80ed?style=flat-square" />
<img alt="targets KIOU" src="https://img.shields.io/badge/targets-KIOU%201.0.1%E2%80%931.0.2-ff66a3?style=flat-square" />
<img alt="targets KIOU" src="https://img.shields.io/badge/targets-KIOU%201.0.1%E2%80%931.1.0-ff66a3?style=flat-square" />
<img alt="platform" src="https://img.shields.io/badge/platform-iOS%2013.0%2B-blue?style=flat-square" />
<img alt="arch" src="https://img.shields.io/badge/arch-arm64%20rootless-555?style=flat-square" />
<img alt="runs" src="https://img.shields.io/badge/runs-client--side%20only-1f9d55?style=flat-square" />
Expand Down Expand Up @@ -179,23 +179,23 @@ server-side. The recommended target is always the **latest supported version**.

### Feature × version matrix

| Feature | 1.0.1 (build 11) | 1.0.2 (build 12) |
|---|:---:|:---:|
| FPS Override | ✓ | ✓ |
| AFK Guard | ✓ | ✓ |
| Analysis Tune | ✓ | ✓ |
| Kifu Autosave | ✓ | ✓ |
| **Account Switching** | — | ✓ |
| Feature | 1.0.1 (build 11) | 1.0.2 (build 12) | 1.1.0 (build 15) |
|---|:---:|:---:|:---:|
| FPS Override | ✓ | ✓ | ✓ |
| AFK Guard | ✓ | ✓ | ✓ |
| Analysis Tune | ✓ | ✓ | ✓ |
| Kifu Autosave | ✓ | ✓ | ✓ |
| **Account Switching** | — | ✓ | ✓ |

Account Switching requires hook sites introduced in 1.0.2; the Jailed/JB build
always targets the **latest** version only (RVAs are pinned at compile time).
The Patched IPA build selects the recipe at build time via `TARGET_VERSION`.
Account Switching requires hook sites introduced in 1.0.2. Every build shape
pins its addresses at build time via `TARGET_VERSION`: the Patched IPA picks
the recipe, and the dylib picks the matching RVA header.

### Platform

| | |
|---|---|
| **Latest supported KIOU** | `1.0.2` (CFBundleVersion 12) |
| **Latest supported KIOU** | `1.1.0` (CFBundleVersion 15) |
| **KIOU minimum iOS** | 10.0 (`MinimumOSVersion` in app bundle) |
| **KiouForge minimum iOS** | 13.0 (requires `UIWindowScene`) |
| **Tested on** | iOS 15.0 – 26, arm64 |
Expand Down Expand Up @@ -238,23 +238,23 @@ Filza, or [TrollDecrypt](https://github.com/donato-fiore/TrollDecrypt)). The
App Store download is FairPlay-encrypted and cannot be patched directly.

```sh
# default (1.0.2)
mkdir -p assets/1.0.2
cp ~/Downloads/Kiou-1.0.2.ipa assets/1.0.2/
# default (1.1.0)
mkdir -p assets/1.1.0
cp ~/Downloads/Kiou-1.1.0.ipa assets/1.1.0/
make ipa
# -> packages/ipa/KiouForge-patched.ipa

# target a specific version
make ipa TARGET_VERSION=1.0.1
make ipa TARGET_VERSION=1.0.2
```

Before building after editing hook sites or after a KIOU update:

```sh
# verify current recipe against a specific version's dump + IPA
PYTHONPATH=shared:. TARGET_VERSION=1.0.2 python3 -m tools.verify_sites \
PYTHONPATH=shared:vendor/KIOU-Hook TARGET_VERSION=1.1.0 python3 -m tools.verify_sites \
--recipe recipes \
--index assets/1.0.2/dump.cs.index.json \
--ipa assets/1.0.2/Kiou-1.0.2.ipa
--index assets/1.1.0/dump.cs.index.json \
--ipa assets/1.1.0/Kiou-1.1.0.ipa
```

7 changes: 6 additions & 1 deletion Sources/KiouForge/Hook/FrameRate.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
static int32_t pickFPS(int32_t value) {
int32_t v = KIOUFeatureEnabled(KIOU_FEATURE_FPS_OVERRIDE)
? KIOUTargetFPS() : value;
if (v != value) {
// KIOU re-applies its own target every ~5 s, so logging every
// override would bury everything else. Only report transitions.
static int32_t lastFrom = -1, lastTo = -1;
if (v != value && (value != lastFrom || v != lastTo)) {
lastFrom = value;
lastTo = v;
IPALog([NSString stringWithFormat:
@"[FPS] set_targetFrameRate %d -> %d (override)", value, v]);
}
Expand Down
46 changes: 25 additions & 21 deletions Sources/KiouForge/Kif/Helpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
// KIOUKifTextFromGameController is the bridge into il2cpp — everything else is
// pure Foundation.
//
// Background: GameController.GetKifuText (RVA 0x5D43D10) returns an in-app
// SUMMARY ("001 ☗ ☗3八飛 … まで、☖後手の勝ち(詰み)"), NOT the standard
// KIF 2.0 that desktop kifu viewers expect. We instead run the canonical
// pipeline KIOU uses internally:
// Background: GameController.GetKifuText returns an in-app SUMMARY
// ("001 ☗ ☗3八飛 … まで、☖後手の勝ち(詰み)"), NOT the standard KIF 2.0
// that desktop kifu viewers expect. We instead run the canonical pipeline
// KIOU uses internally:
//
// GetUSIText(self) → "position startpos moves ..."
// ↓
Expand Down Expand Up @@ -71,18 +71,10 @@
// touched by the fill is ThinkingTimesMicros (per-move clock, queued for v0.4).
// ===========================================================================

// ---------------------------------------------------------------------------
// RVAs (KIOU 1.0.1 build 11). Same source of truth as KiouUsiProxy.
// ---------------------------------------------------------------------------
// RVAs pinned to KIOU 1.0.2 (dump.cs verified 2026-07-02). The previous
// values were 1.0.1 leftovers pointing at unrelated methods — calling
// through them at match-end crashed with a SIGSEGV inside GetUSIText
// (LDR from a bogus X10 loaded from *(gameCtrl+0xB0)).
#define RVA_GAMECTRL_GET_USI_TEXT 0x5D49970 // string GameController.GetUSIText(this)
#define RVA_POSITION_TO_SFEN 0x5D49C70 // string Position.ToSFEN(this)
#define RVA_USIPARSER_PARSE_USI 0x5D5CBB0 // static RecordManager USIParser.ParseUSI(string)
#define RVA_KIFWRITEOPTIONS_CTOR 0x5D5925C // void KIFWriteOptions..ctor(this)
#define RVA_KIFWRITER_WRITE 0x5D59264 // static string KIFWriter.Write(RecordManager, KIFWriteOptions)
// The five methods this pipeline calls are direct-ABI catalog rows in
// KIOU-Hook (hook_id = -1 — resolved and called, never hooked), so their
// addresses come from KIOUHookSiteAddr and follow KIOU_HOOK_TARGET_BUILD
// instead of being pinned to one app version here.

// KIFWriteOptions instance size needed for the raw-buffer trick. See the
// KIFOPTS_OFF_* constants in Internal.h for the field map. Last field
Expand Down Expand Up @@ -159,29 +151,41 @@
static KIFWriter_Write_t g_KIFWriter_Write = NULL;
static Position_ToSFEN_t g_PositionToSFEN = NULL;

// KIOUHookSiteAddr returns 0 for a name the catalog doesn't carry; return
// NULL rather than a pointer to unityBase itself so callers' existing null
// checks catch it.
static void *resolveSite(const char *name) {
uintptr_t addr = KIOUHookSiteAddr(name, g_unityBase);
if (addr == 0) {
IPALog([NSString stringWithFormat:@"[KIF] site unresolved: %s", name]);
return NULL;
}
return (void *)addr;
}

// Resolve the il2cpp NativeFunction pointers we use. Idempotent and cheap;
// safe to call once per export call.
static void resolveIl2cppFunctions(void) {
if (g_unityBase == 0) return;
if (!g_GetUSIText) {
g_GetUSIText = (GameCtrl_GetUSIText_t)
(void *)(g_unityBase + RVA_GAMECTRL_GET_USI_TEXT);
resolveSite(KIOU_HOOK_NAME_GAMECTRL_GET_USI_TEXT);
}
if (!g_ParseUSI) {
g_ParseUSI = (USIParser_ParseUSI_t)
(void *)(g_unityBase + RVA_USIPARSER_PARSE_USI);
resolveSite(KIOU_HOOK_NAME_USIPARSER_PARSE_USI);
}
if (!g_KIFOpts_Ctor) {
g_KIFOpts_Ctor = (KIFWriteOptions_Ctor_t)
(void *)(g_unityBase + RVA_KIFWRITEOPTIONS_CTOR);
resolveSite(KIOU_HOOK_NAME_KIFWRITEOPTIONS_CTOR);
}
if (!g_KIFWriter_Write) {
g_KIFWriter_Write = (KIFWriter_Write_t)
(void *)(g_unityBase + RVA_KIFWRITER_WRITE);
resolveSite(KIOU_HOOK_NAME_KIFWRITER_WRITE);
}
if (!g_PositionToSFEN) {
g_PositionToSFEN = (Position_ToSFEN_t)
(void *)(g_unityBase + RVA_POSITION_TO_SFEN);
resolveSite(KIOU_HOOK_NAME_POSITION_TO_SFEN);
}
}

Expand Down
2 changes: 1 addition & 1 deletion control
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: work.tkgstrator.kiouforge
Name: KiouForge
Version: 0.2.1
Version: 0.2.2
Architecture: iphoneos-arm64
Description: KIOU (Shogi) local quality-of-life tool. Extends the frame-rate preset beyond 30/60, suppresses false AFK warnings during long-think sessions, and lets you tune the depth and hash used by the on-device post-game kifu analysis engine. Runs entirely client-side and never modifies any server-stored data. For authorized testing only.
Maintainer: NEVER KNOWS BEST
Expand Down
9 changes: 5 additions & 4 deletions docs/plans/on-device-smoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ practice. Broader flows can layer on the same harness later.
frida-il2cpp-bridge`, or `pip install frida-tools` if we go Python).
The design below is bun-based to match KiouForge's existing tool
chain in `shared/tools/`.
- **`assets/1.0.2/dump.cs.index.json`** (already checked in) — Frida
script reads class + method names from it so RVAs never appear
literally in the script (they change per app version).
- **`assets/$TARGET_VERSION/dump.cs.index.json`** (already checked in
for 1.0.1 / 1.0.2 / 1.1.0) — Frida script reads class + method names
from it so RVAs never appear literally in the script (they change per
app version).

## Architecture

Expand Down Expand Up @@ -267,7 +268,7 @@ scripts/
case explicitly; user-visible failures still map to "SSH says
frida-server crashed" rather than false test failures.
- **Presenter signatures change per app version.** The script pins
to 1.0.2 today; when 1.0.3 lands, presenter method names or
to 1.1.0 today; when the next build lands, presenter method names or
signatures may drift. Mitigation: keep the mapping (presenter,
method, args) in a small YAML config file, loaded per
`TARGET_VERSION`.
Expand Down
Loading