fix(#260): a schema pattern error names its rule, a bad manifest names its line, a busy dir names its remedy - #294
Merged
Conversation
…names its line, a busy dir names its remedy (#260) Three of issue #260's item-7 messaging gaps. All three were TERSE rather than wrong, so every fix ADDS to the existing message and none replaces it. 1. A `pattern` schema violation was a regex dump sitting beside enum findings that read perfectly, on the field a first-time author gets wrong first: - blockId: 'My First App!' does not match pattern '^[a-z][a-z0-9-]*[a-z0-9]$' `internal/validate/pattern.go` now appends the rule in English and a value that satisfies it. The table is keyed on the REGEX SOURCE, not the field, so one gloss covers every field the schema applies that pattern to; it FAILS SOFT, so a `schema/` sync that adds a pattern degrades to today's message rather than to a stale English claim; and a bidirectional ledger against the vendored schema fails when either side moves. 2. A malformed manifest reported no position. `encoding/json` already knows the byte offset and the CLI was discarding it. `internal/manifest/jsonloc.go` renders `line L, column C` for both offset-bearing decoder errors, from ONE composer every manifest decode goes through. The offset is a BYTE count and a column is a CHARACTER count: slice by bytes, count runes. The guard is a PAIR of fixtures identical in characters and 6 bytes apart, which must report the same column — a single fixture cannot see the bug. 3. `<dir> is not empty — refusing to overwrite` carried no way forward while the README's Troubleshooting section had one. `scaffold.NotEmptyRemedy` now names both remedies and says there is no `--force`, so nobody spends a round hunting for an override that does not exist. The refusal and its exit code are unchanged, and are pinned as controls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…able's direction independently Two guards rested on a single row, measured with the mutation matrix: - The byte-counted-column mutant reddened EXACTLY ONE leaf subtest across the whole module. The internal/cmd pair that looked like a second backstop put its multi-byte text on line 2 and its defect on a bare `}` on line 4, where the column is 1 either way — it observed nothing. Both packages now carry a SECOND pair whose defect sits after the multi-byte run on the same line, with a count floor so a deletion fails loudly. - SWAPPING two gloss rows left TestPatternFindingsCarryTheRuleAndAnExample entirely green: its expectation is derived from patternRules, so it moved with the mutation, and so did its cross-row denial. TestPatternGlossesAreTheRightWayRound spells each rule fragment and example independently, from what the regex MEANS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t one row Measured: the mutation that tags the non-empty-directory refusal as a usage error reddened exactly ONE leaf subtest. app init is the other user of the same refusal — and the command the README's Troubleshooting entry names — so both are driven, and the 'no --force' clause is now asserted at the user-visible surface as well as at the constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 10, 2026
#294 (9862c1e) merged after this doc was written and it implements #283, #284 and #285 — the three follow-ups this doc lists as open. Its body referenced the #260 umbrella rather than the three issue numbers, so GitHub never auto-closed them and they still read as open work. Verified against the merged binary rather than the diff. - State/DONE/follow-ups updated; the three are struck through with the evidence that closed them, and the doc says plainly they need closing on GitHub. - #291 added: a name over 40 chars silently truncates to a COLLIDING blockId. Found by an adversarial review of #267 and reproduced independently. Item 27's residual list says three classes; there are four. - Ranked next steps: item 1 is struck (done), and two facts that currently exist nowhere else are recorded — AGENTS.md is 201 bytes from its hard ceiling so the next item cannot be added without a deliberate eviction, and #267 shipped a breaking change under a `fix:` subject, which goreleaser's subject filter will keep out of the release notes unless someone adds it by hand at tag time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 10, 2026
ZacxDev
added a commit
that referenced
this pull request
Aug 10, 2026
…guards (#292) * docs(handoff): capture the dogfood-2 workstream and the nine vacuous guards Six issues (#255-#260) from the second blind dogfood run, six PRs merged, all verified on `main` by re-running the original symptoms with controls. The durable content is not the fix list. It is the catalogue of NINE guards that could not fail — each green under `make ci`, all 12 PR checks, AND its own author's mutation matrix, and each hiding a different way. Found only by independent passes that rebuilt the mutants instead of reading the reported table. Also records, with the measurements: - instrument validation (a peak-RSS harness that reported 0 MB for everything; a lint control whose `typecheck` errors proved the parser ran and not the linters; `errcheck` being disabled, so a control built from an unchecked-error slip validates nothing) - that `go test`'s default vet subset includes `printf`, so an arity error surfaces as `build failed` and never as `--- FAIL` — hit twice for real, once producing what would have been scored a surviving mutant - that a DECLARED equivalent mutant is a claim needing its own discrimination table; one was wrong on four inputs - the operating traps: zsh word-splitting and history modifiers, a `cd` failure that let a `git merge` run in the base clone, a guard whose non-zero exit the caller ignored, `git rerere` replaying silently - how the AGENTS.md item-25 three-way collision resolved (25/26/27) - four open follow-ups (#283-#286) and two issues closed as DECISIONS - the residuals shipped deliberately, each with its measurement And the coverage gap that matters: both dogfood runs were un-credentialed, so `civitai generate` — the only irreversibly money-spending surface — is structurally unreachable by this method. Two clean runs say nothing about it. * docs(handoff): fold #294 and #291 into the dogfood-2 handoff #294 (9862c1e) merged after this doc was written and it implements #283, #284 and #285 — the three follow-ups this doc lists as open. Its body referenced the #260 umbrella rather than the three issue numbers, so GitHub never auto-closed them and they still read as open work. Verified against the merged binary rather than the diff. - State/DONE/follow-ups updated; the three are struck through with the evidence that closed them, and the doc says plainly they need closing on GitHub. - #291 added: a name over 40 chars silently truncates to a COLLIDING blockId. Found by an adversarial review of #267 and reproduced independently. Item 27's residual list says three classes; there are four. - Ranked next steps: item 1 is struck (done), and two facts that currently exist nowhere else are recorded — AGENTS.md is 201 bytes from its hard ceiling so the next item cannot be added without a deliberate eviction, and #267 shipped a breaking change under a `fix:` subject, which goreleaser's subject filter will keep out of the release notes unless someone adds it by hand at tag time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(handoff): don't spell a hypothetical AGENTS item as a cross-reference My previous commit wrote the literal token "item 28" while describing an item that could not be added because AGENTS.md is at its size ceiling. TestAgentsItemCrossReferencesResolve scans the repo for `item N` and requires each to resolve; AGENTS.md has items 1..27, so it correctly flagged it as dangling and reddened build-test on a docs-only PR. The guard is right and should not be loosened: it cannot distinguish a deliberate reference to a hypothetical item from the renumbering bug it exists to catch, and the renumbering bug is the expensive one. So the prose moves the number out of the `items?[\s-]+[0-9]+` shape instead — "a new item (which would have been the 28th)". Verified with a negative control rather than by re-reading the pattern: re-introducing "item 28" reddens the test with its own message, and the reworded text passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 three of the four remaining code items under #260 § 7. All three were terse rather than wrong, so every fix APPENDS to the existing message and none replaces it.
Base:
8ec3cb0(rebased onto the currentorigin/maintwice — it moved under this branch mid-session).What this closes, and what remains
patternerrors are raw where semantic errors are excellent<dir> is not emptycarries no remedycivitai app packagedoes not exist--fromships aTODO(server):note01c486e)app create --help"validates clean"unknown flagoffers no next stepmain— verified liveapp validate's exit code undocumentedmain— see caveat belowapp metricsrecommends a login that will be refusedmain— verifiedUmbrella items 1–6 of #260 (README TOC, undocumented
list/view/pull, listing-media on-ramp) are untouched.Verification of the three "already done" claims
unknown flaghint — confirmed live:civitai app validate --stict→Error: unknown flag: --stict/Run 'civitai app validate --help' for the available flags, rc 2.app metricsnames the personal API key — confirmed in source and pinned byinternal/cmd/app_metrics_credential_test.go:appMetricsCredentialRoute= "a full-scope personal API key …", and the no-token error says a browser login is 403-refused there.app validate's exit code — confirmed, but it is documented incivitai --help(the root exit-code taxonomy fromexitCodeDocs), not incivitai app validate --help.app validate --helpstill only says "Warnings do NOT fail validation (exit 0) unless--strictis passed" and never states that an invalid manifest exits 1. Deliberately left alone here — it is a docs decision, not a defect, and the contract is published. Flagging it so the umbrella can record it as closed-with-a-caveat rather than closed.1. A
patternviolation now names its rule and shows a valid valueBefore (
main):After:
internal/validate/pattern.go. Three decisions that will attract a "fix":$refor reused on two fields. One gloss covers all seven patterns wherever the schema applies them.schema/sync that adds a pattern degrades to terse, never to a stale English claim.TestPatternRulesCoverTheVendoredSchemais a bidirectional ledger that keeps the two in step anyway — it fails when the schema grows a pattern with no gloss and when a gloss names a pattern the schema no longer has.Residual, stated rather than hidden:
outputDir's fournot: {"pattern": …}sub-schemas are deliberately unglossed and cannot be glossed — a failingnotsurfaces askind.Not, whose message is the barenot failedwith no keyword path, no regex and no value.buildCoherencecovers the two shapes it models (leading/,..);outputDir: not failedstill reaches the author for a backslash separator and a Windows drive prefix. Fixing that needs the schema's$commentsurfaced through the library, which it is not.2. A malformed manifest names its line and column
Before:
After:
internal/manifest/jsonloc.go, one composer (invalidJSON) that every manifest decode goes through —Load, both ofLoadRaw's decodes, andSetBlockID's fallback. Both offset-bearing decoder errors are handled (*json.SyntaxErrorand*json.UnmarshalTypeError, the latter reachable becauseLoadRawdecodes the same bytes twice).🔴 The offset is a BYTE count and a column is a CHARACTER count. Slice by bytes, count runes. The two agree on every ASCII manifest, so a single fixture cannot see the bug — it appears the moment an author writes an accented display name, an em dash or an emoji, and a rune-indexed slice moves the line, not just the column. Measured live,
aaaaaaaavsCafé — 🚀(8 runes each, 8 vs 14 bytes) with the defect after them on line 3:3. The non-empty-directory refusal names its remedy
Before:
After (identical for
app createandapp init, rc 1 in both, unchanged):scaffold.NotEmptyRemedy, a named constant so the message, the README bullet and the guards cannot drift. The exit code does not move (a real directory is not item 26's usage error) and that is asserted witherrors.Is(err, ErrUsage)per AGENTS item 7, across both scaffold verbs.Mutation matrix
15 mutations + a null mutant, each checksum-gated (an edit that did not land reports BROKEN rather than reading as a survivor) and each run against a clean-tree gate before and after. Leaf
--- FAILsubtests counted from-voutput, never an exit code. Run overinternal/{validate,cmd,manifest,scaffold}, re-run aftermake fmt, and re-run again on the rebased tree — numbers below are from the final tree.kind.PatterninvalidJSONinto only SOME decode sites--force" clause🔴 Two rows were fixed because the matrix found them resting on a single subtest — the "a battery rested on one row" shape AGENTS items 24 and 26 record:
internal/cmdpair that looked like a second backstop put its multi-byte text on line 2 and its defect on a bare}on line 4, where the column is 1 either way — it read like a discriminator and observed nothing. Both packages now carry a second pair whose defect sits after the multi-byte run on the same line, plus a count floor. M9 → 3 leaves across 2 packages.TestPatternFindingsCarryTheRuleAndAnExampleentirely GREEN. Its expectation is derived frompatternRules, so it moved with the mutation — and so did its cross-row denial.TestPatternGlossesAreTheRightWayRoundnow spells each rule fragment and example independently, from what the regex means. M3 → 4 leaves.app createwas driven).app init— the verb the README's Troubleshooting entry names — is now driven too.Verification
make cicounted on the rebased tree: 18okpackages, 0--- FAIL, 0build failed, 0no test files, 0 timeout panics, 0^FAIL.gofmt -s -l .silent. Positive control: a deliberately-unformatted file dropped in the tree was listed, then removed and the tree re-checked clean — so the silence is "all clean", not "scanned nothing".make lint—golangci-lintis not on this box (the Makefile errors rather than falling back), so it was run undernix-shell -p golangci-lint: v2.12.2, 0 issues. Positive control: a probe file with an unused func and a badSprintfproduced 2 issues, then was removed.1, malformed JSON1, non-empty dir1— all unchanged.e34f598;git diff e34f598 origin/main -- internal/validate/validate.go internal/manifest/ internal/scaffold/scaffold.gois empty, so they are still valid for this base.Recommendation 1 —
--force: do not add itNot implemented, per instruction, and I would not implement it.
The refusal is not an inconvenience with a missing escape hatch — it is the only thing standing between a mistyped
--dirand an unrecoverable overwrite of a directory the CLI knows nothing about. The two remedies now printed cover every case a--forcewould: scaffold elsewhere, or delete first (which is at least an explicit, reviewable act). What was actually broken was that the CLI never said so, which is what this PR fixes.If it is ever added, the shape that would be defensible is not a blanket
--force: it would have to refuse unless the target already holds ablock.manifest.jsonat its root, so it can only ever re-scaffold an app the author already has — never an arbitrary directory.scaffold.NotEmptyRemedyis the single place the "there is no--force" claim lives, so that change is one constant plus a guard.Recommendation 2 —
civitai app package: add a hint, not a command and not an aliasNot implemented, per instruction. My recommendation, in order of preference:
Add a "moved verb" hint to the existing
unknownSubcommandErrorseam (internal/cmd/root.go), a static map from verbs a newcomer will try (package,zip,bundle,build,publish,deploy) to the real invocation. Today:Cobra's Levenshtein suggester is no help —
packageis nowhere nearsubmit.Why a hint and not the alternatives:
app packagecommand duplicates a pipeline that must stay in lockstep withsubmit's — the item-26resolveProjectDirgate, validate, the item-20 ready-ack advisory print,pkgzip,--out,--skip-validate. Two entry points into one pipeline is the "one rule, one place" hazard, andsubmit's is the money-adjacent copy.submit's flags doespackageaccept? "All of them" makescivitai app package --yesa thing that either submits (dangerous) or errors confusingly; a curated subset is a second flag set to maintain.app packagebecause they had not found it in the docs. That said, the README'sapp submitrow should also spell the word "package" sogrep package README.mdlands — it currently only says--package-onlyinside the flag list.If you would rather do nothing, that is defensible too — the cost is genuinely small and
--package-onlyis documented. What I would not do is add the command or the alias.Note for whoever owns AGENTS.md: it is 236 bytes from its own ceiling
This PR does not touch
AGENTS.md, and that is a forced choice worth surfacing.I wrote an item 28 for these three decisions.
agents_size_test.go(new, from #290) failed and named it:origin/mainat8ec3cb0is 67,799 bytes against a 68,000-byte cap — 236 bytes of headroom. Even a one-line item does not fit.The eviction playbook does not apply to a new item: it requires moving the body verbatim to
claudedocs/decisions/NN-*.mdand pinningsha256of its non-blank lines atagentsSplitBase(c5c3817), and a born-split item does not exist at that commit. Adding asplitItemsrow for it would assert a verbatim move that never happened, andTestSplitDigestsAreTheBaseCommitsTextwould fail anyway.So the full rationale lives in the doc comments of
internal/validate/pattern.go,internal/manifest/jsonloc.go,internal/scaffold/scaffold.goand each guard's header — which is where someone editing that code will read it. The next person who wants an AGENTS item will hit the same wall. Unblocking it means evicting an existing large in-file item first (items 19 and 10 are the biggest, ~7.8 kB each, and both are atagentsSplitBaseso the playbook works for them) — a separate, measurable change I deliberately did not fold in here.🤖 Generated with Claude Code