fix(validate): the ready-ack advisory names the reason it fell back, and wraps - #269
Merged
Merged
Conversation
…and wraps The presence-tier advisory GUESSED at why the entry graph had not resolved — "there is no index.html at the project root, or it holds a reference this CLI cannot follow — a bundler alias, a generated file, an off-project URL" — while `EntryGraph.Gaps` already held the real, per-reference reason and readyack.go discarded it. In the canonical #206 shape (a `static` scaffold whose `civitai-host.js` has been deleted) none of the guesses is true: index.html plainly references a file that is not there. A five-file no-build app sent its author hunting for a bundler alias that cannot exist in it. Issue #258. The gaps are now surfaced GENERALLY rather than special-casing the dangling reference, so all six gap kinds reach the author at once — capped at 3 with the overflow counted out loud, because a silently truncated list reads as "that was all of them". The tiering is unchanged: AGENTS.md item 20's judgement that a missing target is a GAP rather than a decided absence stands. The message was the defect. Second half: `app validate` printed the ~2 kB advisory as ONE 1938-character line. Layout now happens at the printer (internal/cmd/validate_print.go), which fixes every finding rather than the one that provoked it. Wrapping inside the message would have corrupted `--json` — the inverse of item 23: the field comes from the producer, the layout does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p report A mutation found the hole: restoring the shipped speculation to `readyAckAdvicePresenceOnlyHead` while KEEPING the real reasons — the most likely way #258 regresses — reddened 0 subtests, because the absence assertion was scoped to the gap report. `TestPresenceAdviceNoLongerSpeculates` now reads the emitted message at a fixture where every quoted phrase is provably impossible, with a positive control so "says none of the wrong things" cannot be satisfied by a message that says nothing. Records the measurement, the tiering-unchanged boundary and the mutation matrix in AGENTS.md items 18 and 20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…solute paths Audit fixes for #269. Four 🟡 and the 🟢 set. 1. THE FOURTH PRINT SITE. `app_submit.go`'s error loop kept its raw Fprintf, so one `app submit` run printed a 412-rune unwrapped error AND a wrapped warning — two layouts in a run, on the highest-traffic path — which made this change's own "one place fixes every long message" rationale false. Headers stay unwrapped deliberately: they interpolate the directory the user typed. 2. AN ABSOLUTE PATH IN A USER-FACING GAP. The root-index.html gap was the only one of seven not going through relTo, so it printed `stat /abs/.../index.html` — machine-specific noise and a single unbreakable token: measured 120 runes, producing a 136-rune line under a 79-rune budget. `readableErr` renders it relative; the guard checks the root and a token ceiling over four fixtures, so a NEW site cannot reintroduce it. 3. THE CAP COULD BURY THE ACTUAL CAUSE — this PR's own thesis failing in a new shape. Three CDN script tags above a dangling ./civitai-host.js pushed the real bug out of a three-item list whose lead-in claimed the cause was among them. Two fixes: `rankGaps` orders most-likely-cause first (stable, so document order survives within a kind), and the lead stops claiming the cause is present once anything is withheld. 4. THE WRAP SPLIT PASTEABLE REMEDY TEXT. `"pnpm run build"` came out as `"pnpm` / `run build"`. `findingTokens` keeps a double-quoted span whole, with two fallbacks that make it safe: an overlong span splits back into words, and an unbalanced quote cannot swallow the tail. Plus: the cap VALUE is pinned (99 reddened 0 subtests before), `Complete` iff `Gaps` is asserted where it can break, the two guards that cannot fail are labelled as such, and AGENTS.md's "six gap kinds" (seven) and "1938-character line" (pre-fix 1847 / post-fix 1936) are corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ty guard able to fail Delta-audit fixes for #269. F1 (the 🟡): TestGapRankingIsStableWithinAKind could not fail. Its 2-gap fixture sat under Go's insertion-sort cutoff, so `sort.SliceStable` -> `sort.Slice` reddened 0 subtests. Widening alone was NOT enough — with one gap kind the comparator is all-equal and pdqsort leaves the partition untouched, so 40 same-kind gaps still reddened 0. The fixture now mixes two kinds across 30 gaps, which kills the mutant. Stability decides WHICH THREE gaps the capped advisory shows, so the ranking argument rests on it. F2: the per-file size cap was reported as an unreadable file — "make it readable" about a perfectly readable file — and ranked gapUnreadable, so a limit this check imposes outranked a genuine dangling reference in the capped list. errOverCap/errIsDirectory are now sentinels and readGapFor sorts them to gapBudget. F5: the leak guard's "unreadable" row forced the size cap, which returns a plain fmt.Errorf and never reaches readableErr's *fs.PathError branch. Both packages now carry a chmod-000 row, with a root skip and a fixture positive control. F3: the >79-rune justification was misattributed. Measured: app validate's header is 130 runes and DOES interpolate the user's path; app submit's is a CONSTANT 82 with no path; and a 184-rune `Error:` line comes from main.go's CLI-wide error path. Wrapping that last one is a deliberate non-change — it would alter every command's stderr and item 24 pins those byte-for-byte. F4: cited a TestIncompleteIsExactlyHavingGaps that does not exist; the invariant is an assertion inside TestEntryGraphCompleteness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #258.
The defect
In the canonical #206 shape — a
staticscaffold whosecivitai-host.jshasbeen deleted — the presence-tier ready-ack advisory listed reasons it might have
fallen back, and none of them was the actual reason:
Not one is true of a five-file no-build app. The real reason is that
<script src="./civitai-host.js">points at a file that is not there — andEntryGraph.Gapshad already recorded exactly that, per reference, beforereadyAckChecksreturned the constantreadyAckAdvicePresenceOnlyanddiscarded the slice.
The tiering is unchanged and correct. AGENTS.md item 20 deliberately treats
a reference to a missing file as a gap rather than a decided absence, and
records this case as a known trade. The message was the defect.
The fix
Surface
Gapsgenerally, not the dangling reference specially — so all sixgap kinds (dangling reference, bare specifier, off-project URL, unreadable file,
file budget, depth truncation) reach the author from one change:
; and 2 more this message does not list). A silently truncated list reads as "that was all ofthem" — the same class of lie as the guess it replaced.
"this resolver's model of the project is incomplete", a fact about us.
What sets a gap is untouched — only how it reads.
…PresenceOnlyHeadand…PresenceOnlyTail,before the shared remedy rather than after it: the remedy is the longest
fragment, so appending put the one project-specific sentence two thirds of the
way down a wall of generic advice.
Second half: the 1938-character line
The advisory is ~2 kB and
app validateprinted it as one line. Layout nowhappens at the printer (
internal/cmd/validate_print.go), wrapping everyfinding to 79 columns with a hanging indent — one place fixes every long
message, including ones added later.
Wrapping inside
Finding.Messagewould corrupt--json. This is the inverse ofAGENTS.md item 23: the field comes from the producer, the layout does not.
wrapRunesis reused as-is fromexitcodes_doc.go(same package — no export,no edit to that file).
Mutation matrix
--- FAILleaf lines counted from output (never an exit code); every mutationchecksum-gated, so an edit that silently failed to apply aborts instead of
reading as a survivor.
--jsonguard)🔴 One mutant survived the first round and is why an assertion moved.
Restoring the guess to
…PresenceOnlyHeadwhile keeping the real reasons —the most likely regression — reddened 0 subtests, because the absence check
was scoped to the gap report.
TestPresenceAdviceNoLongerSpeculatesnow readsthe whole emitted message at a fixture where every quoted phrase is provably
impossible, with a positive control that the real cause is present so "says none
of the wrong things" is not satisfied by a message that says nothing.
TestReadyAckAdvisoriesStateTheirOwnStrengthstill passes and still can failin both directions (K: 2, L: 1 above). It reads the FIXED bases, so it
structurally cannot see text appended at runtime —
TestGapReportCannotSatisfyAnotherTiersStrengthAssertioncovers that, and killsthe "report leaks
orphan" mutant the strength test cannot.Coverage
staticandpage-vitewith the emitter deleted: the report names thereferencing file (
index.html/src/main.jsx), the specifier and themissing target — asserted on the gap section, not the whole message, since
the shared remedy names
index.htmlin every tier and a whole-messageContainswould be vacuous.no root
index.html, plus an off-project URL.readyAckAdviceUnwired; all shipped templatesstay silent; the strong tiers never acquire the gap apparatus.
--jsonis asserted on the decodedmap[string]any(aContainsoverraw stdout cannot tell a real newline from the
\nescape).make ci--- FAIL: 0 ·build failed: 0 · 18 packagesok·gofmt -s -l .clean over 299
.gofiles (count quoted as the positive control).🤖 Generated with Claude Code