Files
gmgn-skills/skills
EdwardchennnnandClaude Opus 5 5eecc8574b fix(gmgn-heat-rank): close four holes found reviewing the age split
Re-audit of the change in this PR, all four in code or claims the PR itself
introduced.

1. A creation time that was not a readable number crashed the whole run. A
   string-typed timestamp in `created.json` raised `TypeError: unsupported
   operand type(s) for -: 'float' and 'str'` out of `creage()` and took the
   entire scoring pass with it, instead of dropping one row. The loader now
   coerces once, up front.

2. Worse than the crash: the values that did NOT crash were accepted and
   silently moved the list. `NaN` compares False against everything, so it
   passed the track test as "not young" AND came out of the freshness clamp
   at 1.0 — the easier mature track and a full newborn bonus at the same
   time, which is precisely the bug this PR exists to remove. A negative or
   absurdly small number read as "very old" and also bought the mature
   track. Unreadable is now stored as None (asked, no answer), which falls
   back to open time and keeps the row on the stricter track. Verified
   against string / list / NaN / negative / float / null / bool / zero /
   future-dated: nine cases, no crash, and every unreadable one now resolves
   in the strict direction instead of the generous one.

3. A lookup that never landed was indistinguishable from one that came back
   empty, and both were written as null. Exit status cannot separate them —
   `gmgn-cli token info` answers 0 with a fully zeroed body for an address it
   does not know (measured: `creation_timestamp: 0`, empty stderr). So the
   body itself decides: one that parses and echoes the address back is a real
   answer and a zero creation time is written as null, while one that does not
   parse or does not echo it back is a call that never landed, is left out of
   `created.json` entirely, and is named on stdout so Step 3 lists it again
   rather than scoring the row off an age nobody read. Step 3b also keeps its
   stderr now, in an `.err` file beside the response, the way Step 1 does,
   instead of sending it to /dev/null.

4. The report could state an age that was off by weeks. The table's age column
   is the pool-open age, which is right for the 7-day ceiling, but after this
   PR a token can be listed whose real age is far older — and nothing printed
   that number, so the writer could not have known. arc/ARGUS would have been
   reported as "1.9d" on a list whose headline promise is recency, when the
   token is 19.5 days old. The address line now prints `created=N.Nd ago`
   whenever the two differ by more than a day, and stating it is required in
   `## What the answer has to contain`.

Two claims elsewhere in the file that this PR had made false are corrected
rather than left to contradict it: the ceiling bullet said `MAX_AGE_D` stops a
months-old token reaching the list, which is now true only of months of
trading and not of the token's age, and the `YOUNG_D` cliff note did not say
which of the two ages the cliff is measured on.

No threshold, weight, gate constant or routing description changes. Against
the same saved 7-chain sweep, pass 1 names the same 5 rows and pass 2 returns
the same 4 names in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:09:13 +08:00
..