Files
civitai__civitai/scripts/mutation
Zachary Lowden b34d90fd1c fix(apps): make the listing-completeness advisory KIND-AWARE (on-site copy lives in block.manifest.json) (#4370)
* fix(apps): make the listing-completeness advisory KIND-AWARE

`computeListingProblems` was kind-blind. It emitted `empty-description`,
`empty-tagline` and `empty-category` with the label "Missing <field>" for
EVERY listing. On an OFF-SITE listing that is correct — the author typed that
copy into the submit wizard and can go and fix it. On an ON-SITE listing it is
wrong, not merely terse: those scalars have NO author surface other than
`block.manifest.json`, and `approveRequest`'s (3b-sync) MANIFEST-GOVERNED COPY
RE-SYNC (`publish-request.service.ts`, scoped `kind: 'onsite'`) re-derives them
from the manifest on every subsequent-version approve. An on-site author who
found some other way to set a tagline would have it reverted at the next
approve — so `/apps/mine` was telling them to do something that cannot work.

KEEP-WITH-CORRECTED-LABELS, not suppress. The gap is real (the store page
genuinely has no tagline), so hiding it would trade wrong advice for no advice;
and suppression would silently kill the on-site branch of a released CLI that
consumes these codes. The codes and severities are therefore KIND-INVARIANT and
only the three labels move.

`kind` is a REQUIRED input, which is what makes "a caller was missed" a compile
error rather than a whole surface keeping the old advice. All three callers are
threaded, each PROJECTING the column rather than restating what its own filter
implies:
  - appListings.listMine        (app-access.service)     — both kinds, one page
  - appListings.listMySubmissions (offsite-listing.service) — on-site media
    revisions appear here too, via its `{ kind: 'onsite' }` OR-branch
  - blocks.listMyPublishRequests (blocks.router)         — projects `kind` even
    though its `where` filters on it

The kind lookup is an explicit equality branch, not a `TABLE[kind] ?? default`:
`kind` is an untrusted cast off the `app_listings.kind` column, and indexing an
object literal with an inherited key (`'constructor'`) returns something truthy,
which `??` accepts and which then yields a problem with NO label. An
unrecognised kind degrades to the original labels and never throws.

No component change: `ListingProblemsIndicator` renders whatever `label` it is
given.

Tests: 12 regression cases RED at origin/main d345b654a2 / green at HEAD, plus
labelled invariant guards pinning the wire contract. 14/14 mutants killed, each
by its own guard's assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(apps): correct the empty-category DIAGNOSIS, and close three audit gaps

Follow-up to the adversarial audit of #4370. No change to the kind-awareness
itself; the wire contract is untouched (same 8 codes, same severities, off-site
labels byte-identical).

1) empty-category named the WRONG REMEDY on an on-site listing (MEDIUM).

   The old label — and the comment justifying it — argued that an on-site listing
   can only lack a category when `AppBlock.category` is null, so the manifest must
   be the fix. That reasoning skipped a reachable, DESIGNED state: the advisory
   reads `AppListing.category`, and `setMarketplaceMeta` (moderator curation)
   writes `AppBlock.category` ONLY — it never touches the listing row.

     author omits category   -> listing minted null
     moderator curates       -> AppBlock.category set, AppListing.category null
     -> advisory fires; author "sets it in the manifest"; (3a)'s null-gate does
        NOT fire, their value is DISCARDED, and (3b-sync) writes the moderator's.

   The problem firing is right (the store card genuinely shows none); the
   diagnosis was wrong. The remedy that always clears it is an approved new
   version — the manifest key matters only when no category is set anywhere. The
   label now leads with that and marks the manifest conditional, which is why it
   is deliberately asymmetric with description/tagline (for those two the manifest
   IS the whole remedy, re-derived on every sync).

   Enumerating this also turned up a THIRD writer of the column that the audit's
   "exactly two" missed: (submit-draft) in publish-request.service mints the
   pre-approval draft straight from the manifest, because no AppBlock exists yet.
   It does not rescue the old claim — it is a create, not a rewrite of an existing
   null — but the comment now lists all three.

2) A comment claimed a guard that did not exist (LOW-MEDIUM).

   `ListingProblemKind`'s docblock said assignability with `ListingKind` was
   pinned in listing-problems.kind.test.ts. That file held no such assertion. The
   hazard IS covered — by the listMine call site, under the ROOT typecheck — so
   the comment now names the real mechanism, and the test carries a belt-and-
   braces pin that says out loud it is only checked by the deliberate pass.

3) LISTING-vs-REQUEST kind was pinned only structurally.

   The audit's mutant reading `r.kind` instead of `r.appListing.kind` SURVIVED all
   132 tests, because every fixture set the two equal. Not a live hole, but the
   stated rationale is that they might diverge, so a fixture now makes them —
   both directions.

4) The fixture guard enumerated only factory output, not inline literals. It now
   scans this file's own source, with both controls (a planted bad literal it must
   flag, and a non-zero count of literals actually examined).

Battery committed (scripts/mutation/) with its results, so the claims are
auditable from artifacts rather than reconstructible from a table. Two mutants
added: M14 (the audit's survivor) and M15 (reverting the category diagnosis).

Matrix re-measured at origin/main 4bfd4c16d: 15 red at base / green at HEAD.
Mutation: 16 defined / 16 verdicts / 16 killed, every run at the 154 baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:05:17 -05:00
..