Prior-art interaction matrix — Explore RecordSet / query / presentation

redesign-explore-recordset-query-presentationProject noteResearch
Created openspec/changes/redesign-explore-recordset-query-presentation/design-notes/prior-art-interaction-matrix.mdView on GitHub →

One auditable table per the design's load-bearing interactions. Columns: OBSERVED pattern (with PRIMARY SOURCE) | the PRODUCT REASON | PDPP TRANSLATION | ANTI-PATTERN | ACCEPTANCE CHECK. Sources are source-backed, primary product/spec docs where available; a few rows cite secondary write-ups (9to5Google, Blocksender, a GitHub community thread, Tom's Guide) as supporting evidence for an observable product behavior, not as the spec of record (hand-verified; the deep-research harness was rate-limited — see docs/research/explore-query-filter-ia-prior-art-2026-06-21.md). PDPP-SPECIFIC INVENTIONS are flagged: RecordSet, the count_kind enum, and the manifest role vocabulary.

A. Query / filter / search

InteractionObserved (source)Why it worksPDPP translationAnti-patternAcceptance check
One query inputGmail: one bar = free-text + chips + advanced builder; a pasted/typed term resolves, no separate id box (https://9to5google.com/2020/02/19/gmail-search-chips/). Stripe: one search bar does text+filters+id (https://docs.stripe.com/dashboard/search)One place to express intent; no "which box?" decisionONE input; pasted exact id → "jump to record" affordance, not a 2nd boxTwo inputs (current: "search values" + "go to id")Only one query input; Enter submits; pasting an id offers jump
Chips vs operatorsGmail chips == the operator behind them ("Has attachment" = has:attachment), chips on web+mobile (https://support.google.com/mail/answer/7190). Linear click-to-refine chips, operators in the API only (https://linear.app/docs/filters)Recognition over recall; novice and power user build the SAME queryCommon filters = chips w/ typeahead; operators = optional power path producing the same queryRequiring operator syntax as the only path; mixing typed operators INTO a token field ("text mode vs token mode", https://github.com/community/community/discussions/15655)Selecting a chip yields the identical query to typing its operator
Facets vs queryDatadog: facet panel selections reflect in the query bar + URL — ONE state (https://docs.datadoghq.com/logs/explorer/facets/)No "do my checkboxes AND my query both apply?" ambiguitySource/stream facets are part of the ONE query state, in the view linkTwo parallel filtering systems (current confusion)Changing a facet updates the query+link; changing the query updates the facet state
Invert / negateStripe: leading - negates any filter (https://docs.stripe.com/dashboard/search). Gmail: -term + the "Doesn't have" form field (https://blocksender.io/using-boolean-and-and-not-operators-in-gmail-search/). Linear: chip "is not" toggle (https://linear.app/docs/filters)Exclusion is a first-class need; expose it in BOTH UI and syntaxSource/stream invertible via a chip "is not"/exclude toggle AND -No way to invert (current)"Everything except X" is expressible by chip and by operator
Facet countsDatadog: the number = count in the CURRENT filtered query scope, updates as filters change (https://docs.datadoghq.com/logs/explorer/facets/). Stripe: refuses a total it can't cheaply guarantee — no default list total, search total only to 10,000 (https://docs.stripe.com/api/pagination/search)A number must mean one clear thing or it misleadsFacet number = exact count in the current filtered set; if not exactly computable → HIDDENA number whose meaning is ambiguous (current)A shown facet number is exact-for-the-current-set; otherwise absent
KeyboardGmail/Linear: Enter submits; Cmd-K jumpTable stakes; speedEnter submits; Cmd-K for jump/id; typeahead/escapeButton-only submit (current bug)Enter submits without a button
MobileGmail keeps CHIPS on mobile; advanced panel → a filter button/sheet (https://support.google.com/mail/answer/7190)Power survives the small screenChips on mobile + a filter button → bottom sheetLosing filtering on mobile, or a panel that overflowsMobile shows chips + a filter button opening a panel

B. Reachability / collapse / load-more (RecordSet)

InteractionObserved (source)Why it worksPDPP translationAnti-patternAcceptance check
Count == reachabilityStripe: a count is a handle to its exact filtered set via URL state; refuses unreachable totals (https://docs.stripe.com/dashboard/search). Linear: true per-group totals tied to full membership (https://linear.app/docs/filters)A number is a promise; keep itEvery shown count is exact AND reachable, else hidden; never shrunkA count promising more than the UI reaches (Google Photos Stacks "promising completeness it does not deliver" — legibility research)A shown count's set is reachable to its last member
Grouped (burst) countLinear true per-group count (https://linear.app/docs/filters)Tells the owner the real sizeBurst shows the TRUE per-(conn,stream,day) total, not the loaded countShowing the loaded count (current 32) as if completeBurst count == true total of its set
"Show all" for a groupGoogle Photos Stacks: inline expand, hard-capped at 100 (https://www.tomsguide.com/...photo-stacks...). Stripe invoice lines: ≤10 inline, >10 a separate paginated endpoint (https://docs.stripe.com/invoicing/preview)Small = inline; large = drill into the full paginated setInline-if-loaded; else "Open all N →" scope-preserving drill-in/paginate"show all" over a subset it can't complete"Show all" reveals the full set, or becomes a complete drill-in
Load-more in groupsreact-virtuoso GroupedVirtuoso: mutate groupCounts in place, adjust firstItemIndex by new-item count; never displace shown (https://virtuoso.dev/react-virtuoso/api-reference/grouped-virtuoso/)Stable scroll; no disorientationMerge in place; a singles-day crossing the threshold collapses down; no reorder"collapse up" / displacing shown rowsLoad-more never reorders shown rows; partial day collapses down
Future/upcoming sectionThings 3 day-sections, mutually-exclusive lists (https://culturedcode.com/things/support/articles/4001304/). Todoist Upcoming = its own week-paged surface (https://todoist.com/help/articles/plan-ahead-with-upcoming-view-KgKpuaGq)Future is its own thing; one record in one placeUpcoming = own day-sectioned surface w/ own reachability; future records only hereFuture riding the main feed's burst+capEvery upcoming record reachable; none in the main feed

C. Record presentation (manifest-authored)

InteractionObserved (source)Why it worksPDPP translationAnti-patternAcceptance check
Declared title/primary roleAirtable primary field = declared title across views (https://support.airtable.com/docs/the-primary-field). Notion: schema requires exactly one title property (https://developers.notion.com/reference/property-object). schema.org name/mainEntityDeclared, never guessed; arbitrary schema renders rightManifest declares the primary role; renderer reads itGuessing the title from a field-name listA declared-role connector renders the correct title with no client code
Type vs roleAirtable: only primary-eligible TYPES can be the title (type gates role, but role is a separate declaration); interface layouts choose title + 2 preview ROLES (https://support.airtable.com/docs/interface-layout-record-review). JSON Schema title/description are display annotationsA text field can be title OR body — type doesn't say whichTwo axes: field TYPE (timestamp/currency/text/person/media/url/geo) ≠ presentation ROLE (primary/secondary/event-time/actor/amount/media/supporting)Treating type as role (two text fields, no way to say which is the title)The same TYPE can carry different ROLEs; the manifest declares which
Honest generic fallbackDatadog: arbitrary logs → generic key/value attribute table; reserved standard attrs special only when present (https://docs.datadoghq.com/logs/explorer/). Google My Activity + GitHub: generic base-schema (header/title/time) + typed-detail renderer (https://www.gharchive.org/)An honest table beats a confident wrong cardUndeclared record → stream label + declared time + identity + key/value table (humanized labels)A guessed message/money/photo card from field/stream namesAn undeclared stream renders a generic card, never a guessed typed card
Humanized labelsJSON Schema title annotation = the display label (https://json-schema.org/understanding-json-schema/reference/annotations)Deterministic, declaredUse the manifest's declared field label where present; mechanical key-format only as a last-resort labelInferring semantics from a prettified key nameLabels come from manifest annotations; key-format is label-only, never semantic

D. Row actions / selection

InteractionObserved (source)Why it worksPDPP translationAnti-patternAcceptance check
Row click vs OpenAirtable: row = expand/peek; a record has a full detail view (https://support.airtable.com/docs/interface-layout-record-review). Linear: click = peek, full route distinctTwo intents (inspect vs go-to) need two outcomesDesktop: click = peek; Open = full route. Mobile: tap = full routeOpen identical to row click (useless)Open and row click differ on desktop
Drill-in lives at group level(B) scope-preserving drill-inPer-row links are noiseStream door at the group/burst level, not per rowA "view full stream" link on every rowNo per-row stream link; group-level drill-in present

Search-result-set classes (the deepest seam — pins lexical honesty)

ClassDefinitionCount/reachabilitySource/precedent
relevance_boundedA semantic/top-match candidate pool; NOT an exhaustive count of all conceptual matchescount is the POOL SIZE (a bounded sample), lower_bound at most, never "all matches"; no "sort newest" affordance implying completenessStripe search total only to 10,000 (https://docs.stripe.com/api/pagination/search); existing explore-search-result-set-model-validation research
keyword_pageable (exhaustive lexical)A keyword/filter set that can be walked to exhaustion via cursorexact count is provable; fully reachableStripe has_more walk-to-exhaustion (https://docs.stripe.com/api/pagination)
chronological browseThe time-ordered corpus under a scope; no relevance promiseexact; fully paginatedthe deployed merged-timeline (cursor v4)

ACCEPTANCE: a relevance_bounded set SHALL NOT render as an exhaustive "all matches" set nor expose "sort newest" implying completeness; "all records matching X" SHALL create/ navigate to an exhaustive pageable set or state the exact set is unavailable.

PDPP-specific inventions (flagged)

  • RecordSet (the unit-of-truth object) — synthesized from Stripe URL-scoped sets + Linear true-group-counts + the count==reachability bar; no single product names it.
  • count_kind enum (exact|lower_bound|not_counted|hidden) — makes count==reachability machine-checkable; not a single product's API.
  • The manifest role vocabulary (primary|secondary|actor|media + typed timestamp/amount) — mirrors Airtable's title+preview roles + schema.org name/description; the EXACT enum is PDPP's, owner-gated.