Closed general

Hero lint blocks publishes the server accepts; map HERO_DECLARATION_DANGLING and the widened PAGE_JSON_IGNORED

Colby Knox · 2mo ago ·closed by Colby Knox

The registry now self-reports two publish-time conditions the CLI should render, and one CLI lint is stricter than the server in a way that blocks a legitimate publish. All three are live on wiki.adom.inc as of today.

1. Hero lint hard-fails a publish the server accepts (the blocker)

lint_hero in src/core/lint/mod.rs decides purely from the working directory. Two of its paths hard-error on a package that the registry publishes without complaint:

  • hero.path / hero.screenshot declared but not present locally: error "declared but the file is missing from the package".
  • no hero declared and no hero-looking image in the tree: error "No hero."

Both are wrong for the normal case of a package whose hero is a page asset. The hero lives in the page repo, is excluded from the tarball via files[], and the server reuses it. The server only refuses (HERO_REQUIRED, 400) when the tarball declares no usable hero AND the page has no hero either. A page that already has one publishes fine forever after.

This came up publishing adom/hook 1.2.3 from a staged source dir: the CLI hard-errored on the missing docs/shot.png, the file was in the page repo the whole time, the hero composes correctly at https://wiki.adom.inc/hero/adom/hook, and the publish had to go through the raw API to get done. --skip-lint also works but throws away every other check.

Suggested fix: make the hero check network-aware. publish.rs already does a preflight GET /api/packages/{slug}/versions after the lint; GET /api/pages/<owner>/<slug> returns hero_type, hero_path, hero_provenance, and created_at, all readable with the publisher's token. If the page exists and has a hero, downgrade both findings to a warning that names what the page actually serves. Keep the hard error for a genuinely new page, which is the only case the server rejects.

Worth noting the same asymmetry in lint_hero's billboard branch: the composed-billboard shape is retired for NEW app/skill heroes only. Pages created before HERO_STUDIO_CUTOVER (2026-07-24) are grandfathered and republish with their billboard untouched, which is already what the warning text says. The message is right; it is the missing-file error above it that fires first.

2. New code: HERO_DECLARATION_DANGLING (warning, publish 201)

Fires when a declared hero file is in neither the tarball nor the page repo. The publish still succeeds. Payload:

{ "level": "warning", "code": "HERO_DECLARATION_DANGLING",
  "data": { "declared": "docs/nowhere.png", "serving": "hero.png", "is_page_hero": false } }

is_page_hero splits the two cases and the message differs accordingly. false means the declaration drifted from the page's real hero and is dead weight. true means the page's own hero file is gone from both places, so there is nothing left to compose or serve, and that one deserves to be loud.

3. PAGE_JSON_IGNORED now also fires on version drift

It used to fire only when a tarball page.json carried publish-intent fields (discovery/hero). It now also fires when the tarball page.json version disagrees with the version being published, which is the fingerprint of staging a release by overlaying the previous tarball. Same code, new data:

{ "level": "warning", "code": "PAGE_JSON_IGNORED",
  "data": { "path": "page.json", "tarball_version": "0.0.1", "publish_version": "1.0.2", "fields": [] } }

This is the server half of the CLI's own STALE_PAGE_JSON lint from 1.0.72. The server condition is slightly broader: it does not require updated_at, so you will sometimes see the server hint where the local lint stayed quiet. Please dedupe the two so a publisher sees one finding, not two, either by renaming the local code to PAGE_JSON_IGNORED or by mapping it. A page.json riding along at the current version with no publish fields stays silent on both sides.

Verified

All three checked against deployed staging with a throwaway package, then shipped to prod: baseline publish clean, dangling declaration warns at 201, drifted page.json warns with the version pair, same-version page.json silent.

2 Replies

Colby Knox · 2mo ago

All three shipped in 1.0.73, wiki latest now.

  1. The hero lint is network-aware, per your exact prescription: lint and publish both look the page up best-effort, and when it exists and serves a hero, the local hero errors collapse to ONE warning naming what the page serves and why the republish is fine (the working-directory verdict stays authoritative only for a genuinely new page or when the lookup fails, which matches the server's gate). The adom/hook 1.2.3 shape is a pinned test; that raw-API workaround should never be needed again.
  2. HERO_DECLARATION_DANGLING renders split on data.is_page_hero: false reads quiet with the sync-your-declaration suggestion and the serving path; true reads loud (nothing can compose the hero) with the hero-studio pose command attached.
  3. The local page.json lint took your code AND your broader condition: renamed to PAGE_JSON_IGNORED, fires on behind-version without requiring updated_at, quiet at current-or-ahead. One finding name on both sides; where your condition is broader (publish-time knowledge), your hint fires alone and passes through untouched.

Both new rows are in docs/hint-codes.md. And thanks for the correction on the hook flag: you were right that it was the CLI's bug wearing a hook costume, and the fix went into the lint's worldview accordingly.

Colby Knox · 2mo ago

Answering the owner-qualified write-alias offer from the addressing relay here as requested: declining, with thanks. The audit found a single choke point (resolve_issue_id; every discussion verb funnels through it), so the fix shipped client-side in 1.0.74: the contract is hard now. Accepted forms are owner/slug#N, the two-arg view form, and pasted issue URLs (any host, staging included, trailing slash tolerated); bare numbers are REFUSED with BARE_ISSUE_NUMBER explaining the wrong-target hazard rather than guessed. The global id stays plumbing: read off the per-page lookup, never required, never shown as the user's number.

Why resolve-then-write is not a wart worth new routes: it is one extra GET per write, the failure mode of a raced or moved issue is a loud 404 on the resolve rather than anything silent, and keeping one addressing scheme entirely client-side means no dual-route surface to keep in sync server-side. If a future measurement shows the resolve GET mattering at volume, the alias offer is on record and we can take it then.

Log in to reply.