port: a mistyped mod override was silent -- the liveness defect, in the product

Every checker fix this week was about a tool that could not tell 'I checked and it
was fine' from 'I checked nothing'. The port had the same defect facing the person
the asset tree exists for.

ExportTree.resolve announces every shadow as it happens, and its comment already
records why a startup summary was wrong. Nothing reported the opposite. Measured
with two planted overrides, one correct and one in a mistyped directory: the
correct one is announced and the typo produces NO OUTPUT AT ALL. The modder sees
the port load, run, and say nothing about the file that did nothing -- MODDING
rule 4's own failure mode, since base-and-overrides is only usable if an override
that misses says so.

ExportTree.unused_mods() and a report at run end now list them. Controlled both
directions: one inert file with the typo present, silent with it removed.

Getting the category right took three tries and that is the point. v1 'never used'
flagged data/mods/README.md on every run, and a report with a standing false
positive is one nobody reads -- precisely the failure it exists to fix. v2 'no such
path in the export' was correct and still flagged the README. v3 excludes by
extension with the rule checked rather than assumed: the export tree contains only
png, json, ogg, ogv and cmd, verified zero .md anywhere, so a .md in data/mods
could never be an override by construction.

The report also separates what v1 conflated: a file whose path exists in the export
but was not read this run is NOT listed. Every line printed is an override that
can never apply, whatever the run does.

boot.gd already had an _exit_tree and adding a second was a parse error -- the run
failed loudly instead of one hook silently replacing the other, the cheapest
possible failure mode and only because GDScript rejects it.

Their P3 delivery is taken at the strength given: Q6's count-match has disc
support for its structure -- every button record across all 16 GP_TITLE entries is
ptbtn00, ptbtn01-05, ptbtn11-13 -- but it does not show that event 3 is a
particular row, and they said not to author from it. flow.json already binds
buttons by measured screen rather than event index, so nothing changes. Their own
negative is narrower than 'not found': the DIFFICULTY search assumed four items
pair with f variants, so what is established is 'not an 8-record btn-named build
anywhere'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-31 02:20:38 +00:00
parent 3a048de536
commit 7a7bc53893
3 changed files with 161 additions and 1 deletions

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
276 sections. Search this before re-deriving anything.
278 sections. Search this before re-deriving anything.
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
@@ -287,6 +287,8 @@ dies, which is what this file is for.
* [Menu focus does not survive a reboot — and the reach matters more than the result](#menu-focus-does-not-survive-a-reboot--and-the-reach-matters-more-than-the-result)
* [Liveness: every one of my tools passed on an empty input](#liveness-every-one-of-my-tools-passed-on-an-empty-input)
* [Their `ring_row.py` defect, and why it did not reach me](#their-ring_rowpy-defect-and-why-it-did-not-reach-me)
* [The liveness lesson, applied to the product: a mistyped override was silent](#the-liveness-lesson-applied-to-the-product-a-mistyped-override-was-silent)
* [Their P3 delivery, taken at the strength they gave it](#their-p3-delivery-taken-at-the-strength-they-gave-it)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -13966,3 +13968,65 @@ starts mattering.
reason to. Several of my authored values rest on them; what I have instead is
their statement of the exposure and the structural argument for why it did not
fire. Recorded as that, not as verification.
## The liveness lesson, applied to the product: a mistyped override was silent
Every checker fix this week has been about a tool that could not tell *"I checked
and it was fine"* from *"I checked nothing"*. **The port itself had the same
defect, facing the person the asset tree exists for.**
`ExportTree.resolve` announces every shadow as it happens, and its comment already
records why a startup summary was wrong. **Nothing reported the opposite.**
Measured with two planted overrides — one correct, one in a mistyped directory:
```
mod: sprites/title/main_menu/ptbase.png <- data/mods/... ← announced
sprites/title/TYPO_menu/pteff05.png ← NO OUTPUT AT ALL
```
The modder sees the port load, run, and say nothing about the file that did
nothing. **That is MODDING rule 4's own failure mode**: base-and-overrides is only
usable if an override that misses says so.
`ExportTree.unused_mods()` + a report at run end now lists them. Controlled both
directions: **one inert file with the typo present, silent with it removed.**
### 🔴 Getting the report's *category* right took three tries, and that is the point
* **v1 — "never used".** Flagged `data/mods/README.md` on every run. **A report
with a standing false positive is one nobody reads**, which is precisely the
failure it exists to fix.
* **v2 — "no such path in the export".** Correct, and still flagged the README:
it genuinely cannot shadow anything.
* **v3 — excluded by extension, with the rule checked rather than assumed.** The
export tree contains only `png`, `json`, `ogg`, `ogv`, `cmd` — **verified, zero
`.md` anywhere** — so a `.md` in `data/mods` could never be an override *by
construction*. Flagging a class that can never be one is noise.
📌 And the report distinguishes two things v1 conflated: a file whose path exists
in the export but **was not read this run** (a `--menu` run touches one screen) is
**not listed**. Every line printed is an override that can never apply, whatever
the run does.
⚠️ One incident worth keeping: `boot.gd` **already had an `_exit_tree`**, and
adding a second was a **parse error** — the run failed loudly instead of one hook
silently replacing the other. The cheapest possible failure mode, and only
because GDScript happens to reject it.
## Their P3 delivery, taken at the strength they gave it
Q6's count-match now has **disc support for its structure**: every button record
across all 16 `GP_TITLE` entries is `ptbtn00`, `ptbtn0105`, `ptbtn1113` — three
button screens and no fourth, with the other four destinations in their own paks.
⚠️ **Not authored from, and they said not to.** *"It shows the shape the
count-match asserts is real on the disc; it does not show that event 3 is a
particular row."* My `flow.json` already binds buttons to destinations by
measured screen rather than by event index, so nothing here changes — and if a
button-to-event map were ever needed, **there is not one**.
📌 Their negative carries its own reach, which is the part I would have got wrong:
they searched every pak for an 8-button-record build and found none, but the
search assumed DIFFICULTY's four items pair with `f` variants as `GP_TITLE`'s
screens do. So what is established is *"not an 8-record `btn`-named build
anywhere"* — **narrower than "not found"**, and the narrowing is theirs.

View File

@@ -104,6 +104,7 @@ func _ready() -> void:
get_tree().quit(4)
return
var export_tree := ExportTree.locate()
_tree = export_tree
if export_tree.root == "":
push_error(export_tree.error)
get_tree().quit(2)
@@ -735,6 +736,33 @@ func _video_finished() -> void:
var _video_then: Dictionary = {}
## Say what the mods directory contributed, and what it did not.
##
## The shadow lines are printed as they happen; this is the other half -- files
## that matched nothing. Without it a mistyped override is indistinguishable
## from a working one to the person who wrote it, which makes the whole
## base-and-overrides arrangement (MODDING rule 4) unusable by its own audience.
## The export tree, kept so `_exit_tree` can report what the mods directory did
## NOT contribute. It was a local in `_ready`, which is why the unused-override
## report could not exist until now.
var _tree: ExportTree = null
func _report_unused_mods() -> void:
if _tree == null:
return
var unused := _tree.unused_mods()
if unused.is_empty():
return
print("mods: %d file(s) in data/mods can shadow NOTHING -- no such path in the"
% unused.size() + " export:")
for rel in unused:
print(" inert: %s" % rel)
print(" ⚠️ These are not \"not reached yet\": a file whose path exists in the")
print(" export but was not read this run is NOT listed. Every line above is")
print(" an override that can never apply, whatever the run does.")
func _unhandled_input(event: InputEvent) -> void:
# HANDOFF Q9, measured: one (A) press skips a movie -- the title was reached
# at 57 s against a 193 s baseline.
@@ -1204,6 +1232,10 @@ func _start_recording() -> void:
func _exit_tree() -> void:
# 🔴 boot.gd ALREADY HAD an `_exit_tree`, and adding a second was a parse
# error rather than a silent override -- the one failure mode that costs
# nothing. The mods report hangs off the existing hook.
_report_unused_mods()
if _record == null:
return
_record.set_recording_active(false)

View File

@@ -80,6 +80,70 @@ func resolve(rel: String) -> String:
return root.path_join(rel)
## Mod files that were never used, listed at the end of a run.
##
## 🔴 A MISTYPED OVERRIDE WAS SILENT. `resolve` announces every shadow as it
## happens -- that half was already right, and its comment records why a startup
## summary was wrong. What nothing reported was the opposite: a file sitting in
## `data/mods/` whose path matches no asset. Measured: `sprites/title/main_menu/`
## is announced, `sprites/title/TYPO_menu/` produces **no output at all**. The
## modder sees the port load, run, and say nothing about the file that did
## nothing.
##
## That is MODDING rule 4's own failure mode -- base-and-overrides is only usable
## if an override that misses says so -- and it is the same shape as the
## checkers that passed on an empty input: **agreeable rather than wrong.** A
## port that cannot tell "your override is in effect" from "your override was
## never looked at" is unusable for the person the asset tree exists for.
##
## ⚠️ Reported at the END of a run, not at startup: resolution is lazy, so before
## the assets are read there is nothing to compare against. A run that quits
## early will list files a longer run would have used, and the wording says so
## rather than calling them errors.
func unused_mods() -> PackedStringArray:
var out: PackedStringArray = []
if mods == "":
return out
var stack: PackedStringArray = [""]
while not stack.is_empty():
var rel := stack[stack.size() - 1]
stack.remove_at(stack.size() - 1)
var dir := DirAccess.open(mods.path_join(rel))
if dir == null:
continue
dir.list_dir_begin()
var name := dir.get_next()
while name != "":
var child := rel.path_join(name) if rel != "" else name
if dir.current_is_dir():
stack.append(child)
elif not shadowed.has(child):
# 🔴 TWO DIFFERENT THINGS, and reporting them as one produced a
# permanent false positive on the mods directory's own README.
# A file whose path exists in `export/` was simply not read this
# run -- a `--menu` run touches one screen. A file whose path
# exists NOWHERE in the export can never shadow anything: that
# is the mistyped override, and it is the only one that is a
# defect. A report with a standing false positive becomes
# scenery, which is the failure this whole report exists to fix.
# ⚠️ And a THIRD category, excluded by extension with the rule
# stated rather than assumed: the export tree contains only
# `png`, `json`, `ogg`, `ogv` and `cmd` files -- checked, no
# `.md` anywhere -- so a `.md` in `data/mods` cannot shadow
# anything BY CONSTRUCTION and is documentation, not a failed
# override. Flagging a class that could never be an override is
# noise, and a report with a permanent false positive is one
# nobody reads. `data/mods/README.md` is the standing case.
var ext := child.get_extension().to_lower()
if ext in ["png", "json", "ogg", "ogv", "cmd"] \
and not FileAccess.file_exists(root.path_join(child)):
out.append(child)
name = dir.get_next()
dir.list_dir_end()
out.sort()
return out
# `authored/` sits beside `export/`, never inside it: it is hand-written and
# committed, and a re-export must not be able to touch it.
func authored(name: String) -> Variant: