Files
jackwener__opencli/docs/conventions/listing-detail-id-pairing.md
jakevin 4de1b42ab7 chore(convention): retire listing↔detail id pairing CI gate, keep advisory (#1316)
PR #1297 introduced a CI gate that fails when a site has both a listing
and a detail command but the listing rows don't carry an id-shaped column.
The gate came with a 10-entry EXEMPT map (topic-string trending,
profile-attribute rows, UI-only sessions, ...) where each exemption
recorded a "why this listing legitimately doesn't pair" reason.

By the same filter that closed PR #1311 (write-without-delete-pair gate):

  Is "listing should pair with detail" a *permanent* anti-pattern, or
  case-by-case business judgment?

It's case-by-case. Topic-string listings and profile-attribute rows
genuinely don't pair with a detail command. The fact that we needed an
EXEMPT map with 10 entries and individual reason strings is the smell —
it's not the rule winning, it's the rule failing. Forcing every adapter
PR to either add an id column or file an exemption was a higher cognitive
cost than the silent-loss bugs the rule actually catches.

Changes:

- .github/workflows/ci.yml — drop the "Check listing↔detail id pairing"
  step. Other gates (silent-column-drop, typed-error-lint) stay in place.
- package.json — rename the script from `check:listing-id-pairing` to
  `advise:listing-id-pairing` to make the advisory nature explicit.
- scripts/check-listing-id-pairing.mjs — drop the `--strict` flag and the
  EXEMPT map. The script now always exits 0 and prints an advisory report
  of listings that don't carry an id-shaped column. Reviewers/authors use
  it as guidance, not a gate.
- docs/conventions/listing-detail-id-pairing.md — rewrite from "MUST" to
  "soft convention". Adds an explicit "why advisory, not a gate" section
  that lists the legitimate non-pairing categories so future readers know
  the rule's boundary.
- docs/developer/ts-adapter.md — match the advisory tone in the
  adapter-author guidance.

The doc, the script, and the column patterns table all stay — agents and
adapter authors can still consult them. What's gone is the CI failure and
the per-PR exempt-list maintenance burden.

Net diff: -34 lines (gate + EXEMPT map removed, advisory-tone doc adds
a small "why advisory" section).
2026-05-05 00:34:18 +08:00

5.9 KiB

Listing↔detail ID pairing (advisory)

Status: advisory convention, not a CI gate. Use as guidance when designing listing columns; an audit script (npm run advise:listing-id-pairing) can surface candidate listings for human review.

When a site exposes both a listing-class command (search / hot / recent / top / feed / ...) and a detail-class command (read / paper / article / job / view / ...), it's usually nicer for agents if the listing rows include an id-shaped column whose value can be passed directly into the detail command.

Without that, the agent has to either re-search by title or scrape a URL out of band to follow up on a listing row.

Soft convention

If <site> has both a listing command and a detail command, prefer giving each listing row a column whose value round-trips into the detail command's positional argument.

That column is whatever the detail command expects:

Site Listing Detail Round-trip column
hackernews top / best / ask / jobs / show / new read <id> id
hupu hot / search detail <tid> tid
lobsters hot / active / newest / tag read <id> id (Lobsters short_id value)
arxiv search / recent paper <id> id
stackoverflow hot / search / bounties / unanswered read <id> id
openreview search / venue paper <id> / reviews <forum> id
devto top / tag / user read <id> id
bilibili hot video <bvid> bvid
reddit hot read <id> id
bluesky user thread <uri> uri
1688 search item <url-or-offer-id> offer_id
tieba search read <id> id
jike feed / search / user post <id> id
weibo feed / search post <id> id

Why this matters

The agent never has eyes on a webpage. It sees the listing rows as a plain table of strings. If the listing emits [rank, title, author, votes] and the detail command needs an id, the agent has three bad options:

  1. Re-search by title — fragile, may hit a different post for ambiguous titles, costs an extra round-trip plus quota.
  2. Parse the URL — assumes URL shape stays stable and is regex-able, often breaks across A/B buckets or after a site redesign.
  3. Hand-craft a search — pure guess; agents trained to do this leak the failure mode silently downstream.

Surfacing the id in the listing collapses all three into a single, unambiguous column.

Why this is advisory, not a gate

Whether a listing should pair with a detail is a case-by-case product/UX call. Many legitimate listings genuinely don't pair:

  • Topic-string listings (twitter trending, weibo hot) — rows are search keywords, not addressable entities.
  • Profile-attribute listings (reddit user, lesswrong user, weibo user) — rows are [field, value] pairs of one profile.
  • UI-only sessions (discord-app search, notion search Quick Find) — page ids aren't extractable from the rendered DOM.
  • Comment / reply listings — sub-resources of a parent thread; the detail command fetches the parent, not the comment.

A hard CI gate forced authors to either add an artificial id column or file an exemption with a reason — both of which were higher cognitive cost than the silent-loss bugs the rule actually catches. See PR #1311 thread for the broader "anti-pattern vs case-by-case" filter.

What counts as an "id-shaped column"

The advisory script (scripts/check-listing-id-pairing.mjs) considers any of these column names a valid round-trip handle:

  • Exact id / short_id
  • Anything ending in _id or Id (e.g. offer_id, paperId)
  • Domain-specific ids: jk (indeed), tid (thread id), bvid / aid (bilibili), sku (retail product SKU), asin (amazon), isbn, doi, slug
  • username / handle (only when the detail command keys off the user rather than a post)
  • url — only when the detail command's positional argument explicitly accepts a URL (for example read <url-or-id>, article <url>, or help text such as Post ID or full URL). URL is not a valid substitute when the detail command only says an id comes "from URL"; callers would still need to parse the URL out of band.
  • uri — for sites whose canonical handle is a URI scheme (e.g. Bluesky's at://did:.../app.bsky.feed.post/...).

If the natural id for your site is a different shape, add the pattern to ID_COLUMN_PATTERNS in the script.

How to add an id column to a listing

  1. Find the source field. Most listing endpoints already include the id in their raw response — they're just dropped before the row is shaped. Check func's mapper before adding new fetch logic.

  2. Pick the column position. Convention is [rank, id, ...rest, url] so identifiers stay near the front and the URL stays last.

  3. Update the docs. The site's docs/adapters/browser/<site>.md should list the new column under "Listing columns" and call out that it round-trips into the detail command.

  4. Optional: run the advisory script to confirm the manifest reflects the new column.

    npm run advise:listing-id-pairing
    

Advisory script usage

# Print a report of listings that don't currently carry an id-shaped column.
# Always exits 0 — does not fail CI.
npm run advise:listing-id-pairing

The script reads cli-manifest.json, so always rebuild the manifest (npx tsx src/build-manifest.ts) after touching adapter columns.

  • docs/developer/ts-adapter.md — adapter authoring conventions, including the required access: 'read' | 'write' metadata.
  • "Output design" reference inside the bundled opencli-adapter-author skill — column naming, ordering, and id-first conventions.