Back to Rail Game
SOURCE / PINNED RELEASE

Made of little things.

Rail Game

Release
ccdd5b913413…
Author-recorded commit
ddf0122b6acc…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-91ec90c6fdf

Archive hash verified: dad7b678e25f96a3…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

docs/implementation-issues.md
# Implementation issues: starter, tooling and agent guidance

Date: 2026-09-22

Scope: issues encountered while building the first Rail Game prototype, through
commit `fc8ad71`. The initial scaffold is preserved in commit `907d5a5`.

Environment: soyLI 0.18.1 on macOS ARM64; installed `@napplet/sdk` 0.24.4,
`@napplet/nap` 0.28.0, and `@napplet/conformance-cli` 0.2.15.

The prototype was completed and verified. The most consequential problems were
an inherited document that failed the repository's own checks and an incomplete
reference runtime that made a valid SDK integration fail during startup. Other
findings concern ambiguous guidance and gaps in what the checks establish.

Priority below reflects impact on future implementation and verification, not a
security severity. Recommendations have not been applied upstream or submitted
externally. This follow-up adds a report only.

| ID | Finding | Priority | Resolution during implementation |
| --- | --- | --- | --- |
| 1 | Starter documentation fails its own guidance test | High | Corrected the local document |
| 2 | Reference runtime advertises unimplemented domains | High | Added defensive startup handling; tested real host separately |
| 3 | Conformance mislabels an app exception as failed global injection | Medium | Identified the actual error; harness remains unchanged |
| 4 | Green conformance leaves half the checks unmeasured | Medium | Reported skips and added host/browser verification |
| 5 | Preview iframe minimum height invalidates naive tiny-frame tests | Medium | Corrected the browser fixture and asserted actual frame dimensions |
| 6 | Settings guidance mixes protocol status with package/host support | Medium | Followed local integration mapping and preserved SDK fallback |
| 7 | Setup and preview instructions require translation between environments | Low | Used the existing scaffold, installed skills and soyLI commands |
| 8 | Compact control defaults conflict with comfortable mobile targets | Low | Documented a game-specific size exception |

## 1. The starter's action guide failed the guidance check

**Evidence.** The first `pnpm verify` run failed the test “rejects retired
app-owned bootstrap and probing APIs” in [tests/guidance.test.mjs](../tests/guidance.test.mjs).
The offending text was already present in
[docs/napplet-actions.md](napplet-actions.md): it instructed callers to use the
`napplet` → `shell` → `ready` and `supports` methods. The initial text is available
with `git show 907d5a5:docs/napplet-actions.md`.

Those instructions contradicted the installed
[SDK skill](../.agents/skills/napplet-sdk/SKILL.md), the other boundary guidance,
and the repository's explicit regression check. This was an inherited problem,
not a forbidden API introduced by the game.

**Impact.** Verification stopped before type-checking and building. Following the
example would also have directed the implementation away from the documented
SDK/injected-domain boundary.

**Resolution.** Replaced that paragraph with the existing `runtimeHasDomain`
pattern and SDK operation error handling. The guidance test then passed.

**Recommendation.** Run the complete guidance suite against the assembled soyLI
starter after integration documents are added, not only against upstream
boilerplate. Update the distributed action guide to match its SDK contract.

## 2. The reference runtime advertises domains without their SDK methods

**Evidence.** The first conformance run reported the startup exception
`b(...).getItem is not a function`. Inspection of the installed
[reference host bundle](../node_modules/@napplet/conformance-cli/dist/host-bundle.global.js)
confirmed the cause: `runtimePrelude` creates an empty object for each advertised
domain, then implements resource methods. It does not install storage methods.
The [SDK wrapper](../node_modules/@napplet/sdk/dist/index.js) forwards `getItem`
directly to the injected storage object's method.

Consequently, `runtimeHasDomain('storage')` succeeds while the subsequent SDK
operation throws synchronously. Domain absence and a present-but-incomplete domain
are different conditions; the usual presence guard handles only the former.

**Impact.** Optional progress loading could terminate startup under the reference
harness, although the same storage integration worked in the actual soyLI host.
The initial app error handler covered a rejected promise, but the exception
occurred while evaluating the SDK call before that promise handler was attached.

**Resolution.** [src/main.ts](../src/main.ts) now invokes the startup read inside
a promise callback and catches synchronous exceptions as well as rejections.
Failure produces session-only records and leaves the game playable. A real-host
playthrough separately verified saving a score, unlocking stage two and retaining
the unlock after reload.

**Recommendation.** The reference runtime should inject SDK-compatible methods
for every domain it advertises, or omit domains it does not implement. Reuse the
maintained runtime shims where possible. Optional-operation examples should also
show a `try`/`await` block or equivalent that covers synchronous SDK failures.

## 3. The conformance injection error was misleading

**Evidence.** The same run also failed `boot/installs-global`, claiming the runtime
had not injected its global before loading the app. The reference host source
actually assigns `installedGlobal` from `bootError === null`; it does not measure
injection independently. The global had been injected, but the app then threw
because of the storage stub described above.

**Impact.** One exception appeared as two failures with different apparent causes.
The injection message could send an implementer toward changing bootstrap code,
even though the SDK bootstrap boundary was correct.

**Resolution.** Used the concrete storage exception to diagnose the failure. The
local dependency was not patched.

**Recommendation.** Measure global installation separately from app startup and
keep the two diagnostics independent. A later application exception should not be
reported as proof that injection failed.

## 4. Passing local conformance does not establish full protocol coverage

**Evidence.** Final `pnpm test:conformance` output: **5 passed, 0 failed, 5 skipped**.
The three manifest checks had no resolved signed manifest; the wire check had no
emitted reference-shell envelopes; lifecycle cleanup was not measured.

These skips are documented in the [test skill](../.agents/skills/napplet-test/SKILL.md)
and repository instructions. They are an expected coverage limitation, not five
additional failures. However, the final “CONFORMANT” label alone is easy to
misinterpret. The stub behavior in finding 2 also limits meaningful SDK operation
coverage in this particular reference runtime.

**Impact.** A green command would have been insufficient evidence for real progress
saving, live settings, accurate resizing, or teardown behavior.

**Resolution.** Recorded the skipped checks explicitly in
[prototype-verification.md](prototype-verification.md), ran `soyli check`, and
exercised the built app in the real injected iframe. Added simulation and browser
smoke tests. These complement conformance; they do not turn the skipped manifest,
wire or lifecycle checks into measured passes.

**Recommendation.** Provide a documented local full-evidence recipe for supplying
a test manifest, exercising SDK operations and measuring teardown. Make the CLI's
final label distinguish a partial pass from a fully measured pass.

## 5. The host's minimum iframe height hid incorrect viewport assumptions

**Evidence.** An initial browser test requested 200×160 and 240×280, but the app's
measured inner dimensions were 200×300 and 240×300. The host's iframe minimum
height overrode the test's requested CSS height.

**Impact.** Resizing the browser and naming the screenshot after the requested
size would have falsely suggested the mandated tiny-frame checks had run at the
actual target dimensions. This also affects the interpretation of phone-sized
browser screenshots when host controls consume vertical space.

**Resolution.** [tests/browser-smoke.mjs](../tests/browser-smoke.mjs) temporarily
expands the existing host iframe to the viewport, clears its minimum dimensions,
and asserts the app's actual `innerWidth` and `innerHeight`. The exact-size matrix
was rerun successfully. Injection and sandbox attributes remain those of the real
host; the fixture is not a replacement runtime.

**Recommendation.** Add an exact applet-frame sizing mode to the preview, and have
the UI skill require assertions of measured frame dimensions. Report browser
viewport size and actual iframe size separately when testing the full host UI.

## 6. Settings instructions blur protocol and implementation boundaries

**Evidence.** [docs/design-patterns.md](design-patterns.md) and the
[SDK skill](../.agents/skills/napplet-sdk/SKILL.md) describe a gap in standardized
build-time configuration-schema metadata and emphasize runtime registration.
Meanwhile, [docs/napplet-space.md](napplet-space.md) documents this toolchain's
schema embedding, and the initial [settings module](../src/napplet-settings.ts)
already depended on that integration. The build skill mentions optional schema
configuration, while the guidance test explicitly rejects the `configSchema`
option in Vite configuration and accepts the local schema file.

**Impact.** An implementer must distinguish three questions: what the protocol
standardizes, what the installed package implements, and what this host consumes.
Read literally without the local mapping, the guidance could prompt removal of
working settings support or introduction of a prohibited Vite option.

**Resolution.** Kept the existing local schema file and the runtime registration
fallback, preserved the host-supplied schema when present, and replaced only the
demo settings. The actual host form, live updates and reload behavior were tested.

**Recommendation.** Add one versioned compatibility table covering protocol
status, package behavior and host behavior. Explicitly identify local extensions
and the canonical authoring path. This report records the pinned documentation
ambiguity; it does not claim that the current upstream protocol was independently
rechecked during this follow-up.

## 7. Generic skill commands differ from this workspace's commands

**Evidence.** [AGENTS.md](../AGENTS.md) contains an instruction to install the
skills. The upstream make/build/test skills discuss the `napplet` creator,
initialization, deployment metadata, and Kehto/Paja previews. In contrast,
[docs/napplet-space.md](napplet-space.md) says this project is already scaffolded,
its skills are installed, and its supported workflow is soyLI with `napplet.json`.
The original README also presented raw Vite development as the starting command.

**Impact.** Blindly following an individual generic skill could cause unnecessary
installation, attempts to re-scaffold, use of the wrong metadata path, or testing
an asset server instead of the actual sandbox.

**Resolution.** The instruction to read the local integration guide first resolved
this correctly. Used the existing scaffold, existing skills and `soyli dev`.
Updated the product README accordingly. No missing Paja installation was treated
as a blocker because soyLI provided the required real runtime.

**Recommendation.** Keep upstream skills reusable, but add a short explicit
precedence/mapping block at the local agent entry point. Make installation and
scaffolding steps conditional on project state. The existing integration guide is
useful; the main improvement is making its overrides harder to miss.

## 8. Compact defaults require a deliberate mobile exception

**Evidence.** [AGENTS.md](../AGENTS.md) specifies 28px controls, and the
[UI skill](../.agents/skills/napplet-ui/SKILL.md) recommends 28–32px controls as
compact defaults. [docs/napplet-mobile.md](napplet-mobile.md) calls for touch
targets at least 44 CSS pixels wide/high.

**Impact.** Applying the compact numbers uniformly would make repeated game
steering uncomfortable on a phone. This was resolvable policy tension, not an
unconditional contradiction: the instructions allow written product exceptions.

**Resolution.** Documented 44px game touch targets in [game-spec.md](game-spec.md)
and used larger steering controls on narrow/touch layouts. Condensed the smallest
supported frame separately.

**Recommendation.** Separate visual density from interactive target size in the
UI skill, and make the mobile override explicit for games and coarse pointers.

## Environment friction and implementation mistakes

These also occurred, but should not be attributed to broken starter code or
incorrect napplet protocol guidance:

- **Filesystem/process permissions.** The initial soyLI preview could not change
  permissions on its cached pnpm executable. Conformance could not bind a local
  port, Chromium could not register its macOS process service, and the final Git
  checkpoint could not create its index lock under the restricted environment.
  Approved escalated runs succeeded. No automatic approval review rejection
  occurred, and no app sandbox boundary was weakened.
- **My initial theme assumption.** I used a `mode` property that the installed
  `Theme` type does not have. TypeScript caught it. I corrected the code to use
  the supplied colors. This was my implementation error, not a faulty SDK type.
- **My test-harness mistakes.** One script looked for the iframe too early; another
  searched for the visible Settings text instead of its accessible name,
  “Napplet settings.” Both scripts were corrected. Neither was an app failure.
- **Recorder constraints.** The documented eight-second landscape recipe produced
  a nine-second file with the recorder's appended final frame. Both the file and
  complete playback were checked. soyLI has no portrait recording preset; a
  portrait screenshot was captured, but no portrait clip was produced. This was
  a documented limitation, not an unexpected recorder failure.

No new NAP, app-owned network access, signer integration or shell implementation
was needed. The central sandbox and SDK-first instructions were workable. The
highest-value maintenance work is to fix the distributed action guide, make the
reference runtime accurately advertise its capabilities, and correct its startup
diagnostics.

## Campaign expansion follow-up — 2026-09-23

The eight-stage expansion exposed two small assumptions in the existing product:
the HUD denominator was hardcoded to `03`, and the final victory text said
“Three wardens.” Both now derive their count from the stage definitions. The
progress parser already pads score arrays from the current stage list; an added
regression test confirms that older three-stage values remain readable. The old
save format does not distinguish a final-stage loss score from a completion, so
it cannot safely auto-unlock stage four from that score alone.

My first eight-button picker layout also triggered CSS Grid intrinsic-width
overflow at 240×280. The browser check caught the clipped primary button. A
`minmax(0, 1fr)` overlay track and `min-width: 0` briefing fixed it; the short narrow
layout now scrolls its stage buttons horizontally. The corrected frame and access
to the eighth button were verified. This was an implementation regression found
and fixed during the update, not a skill or host defect.

No new instruction conflict or missing host capability was encountered. The
existing local soyLI command mappings and optional SDK boundaries still apply.

## Release preparation follow-up — 2026-09-23

- **Creator mismatch, resolved.** soyLI 0.18.2 initially rejected the publication
  dry run with `CREATOR_MISMATCH`. Another saved account was active. Selected the
  existing account matching this project's creator binding; the public dry run
  then passed. No creator identity, identifier or destination was replaced.
- **Capture timing, fixed in the harness.** With the Playwright clock paused,
  resizing clears the canvas immediately, while a real-time wait does not advance
  the app's animation callback. The first portrait screenshot therefore contained
  an empty canvas. The capture scripts now advance virtual time after resizing;
  the release asset script also checks dimensions and painted canvas pixels before
  saving. This was a test-capture error, not a runtime gameplay failure. Fresh
  landscape and portrait release images replace that failed capture.
- **Maintained guidance refreshed.** The installed CLI had moved to 0.18.2 since
  the earlier work. Its updater added useful verification, minimum-iframe sizing,
  mobile hit-area and process-ownership guidance. It preserved the already edited
  action guide. The game regression suite now runs within the normal `verify`
  command. Conformance and real-host evidence remain reported separately.