Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .claude/skills/search/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: search
description: The search surface across the platform is the `search-entry` API — realm endpoints `/_search` + `/_federated-search`, the host resource `getSearchEntriesResource`, the `<SearchResults>` component (provided to cards as `@context.searchResultsComponent`), and the `RenderableSearchEntryLike` row view-model. Use whenever adding a search/query call site, choosing which search API to call, reviewing or refactoring search code, or writing a card that lists/queries other cards.
description: The search surface across the platform is the `entry` API — realm endpoints `/_search` + `/_federated-search`, the host resource `getSearchEntriesResource`, the `<SearchResults>` component (provided to cards as `@context.searchResultsComponent`), and the `RenderableSearchEntryLike` row view-model. Use whenever adding a search/query call site, choosing which search API to call, reviewing or refactoring search code, or writing a card that lists/queries other cards.
---

# Search — the `search-entry` API
# Search — the `entry` API

Search is **one engine** exposed as the **`search-entry` API**. A `search-entry`
Search is **one engine** exposed as the **`entry` API**. An `entry`
is a heterogeneous result: the engine prefers prerendered HTML (the fast path)
and falls back to a live serialization per row. **The governing invariant: a
consumer never assumes whether a result came back as prerendered HTML or a live
Expand All @@ -27,7 +27,7 @@ card — it renders the entry transparently.**

- `/_search` (single realm — `Realm.searchEntriesResponse`) and
`/_federated-search` (realm-server — `handleSearch`) emit the
`search-entry` document natively (heterogeneous `html` / `item` results).
`entry` document natively (heterogeneous `html` / `item` results).

## Host

Expand Down Expand Up @@ -55,9 +55,9 @@ import {
type SearchEntryWireQuery,
} from '@cardstack/runtime-common';

// `@query` is a `search-entry`-rooted query (`SearchEntryWireQuery`).
// `@query` is an `entry`-rooted query (`SearchEntryWireQuery`).
// Build one from an ordinary `Query` with `searchEntryWireQueryFromQuery`,
// then add `realms` / `page` / a `fields[search-entry]` fieldset as needed.
// then add `realms` / `page` / a `fields[entry]` fieldset as needed.
get query(): SearchEntryWireQuery {
return {
...searchEntryWireQueryFromQuery({
Expand Down
4 changes: 2 additions & 2 deletions docs/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ let { data: matching, meta } = await indexer.search({

## HTTP API

The TypeScript API described above is exposed by the realm server over HTTP as the `search-entry` API: `/_search` at a realm root (a single realm) and `/_federated-search` on the realm server (across realms). Both speak the `search-entry` wire query — build one from an ordinary `Query` with `searchEntryWireQueryFromQuery` — sent as the request body with the `QUERY` method. An Accept header of `application/vnd.card+json` must be sent.
The TypeScript API described above is exposed by the realm server over HTTP as the `entry` API: `/_search` at a realm root (a single realm) and `/_federated-search` on the realm server (across realms). Both speak the `entry` wire query — build one from an ordinary `Query` with `searchEntryWireQueryFromQuery` — sent as the request body with the `QUERY` method. An Accept header of `application/vnd.card+json` must be sent.

### Example

Expand Down Expand Up @@ -228,4 +228,4 @@ let response = await request
.send(searchEntryWireQueryFromQuery(query, { fields: ['item'] }));
```

The response is a `search-entry` collection document: each entry resolves to prerendered HTML (the fast path) or a live serialization, and `fields: ['item']` asks for the full card/file serialization in `included`.
The response is an `entry` collection document: each entry resolves to prerendered HTML (the fast path) or a live serialization, and `fields: ['item']` asks for the full card/file serialization in `included`.
2 changes: 1 addition & 1 deletion packages/base/Skill/boxel-development.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ export interface CardContext<T extends CardDef = CardDef> {
};
};
}>;
// The search rendering surface: renders the heterogeneous `search-entry`
// stream for a `search-entry`-rooted query — prerendered HTML inert (hydrated
// The search rendering surface: renders the heterogeneous `entry`
// stream for an `entry`-rooted query — prerendered HTML inert (hydrated
// lazily) or a live card — so a card author renders results without ever
// branching on prerendered-vs-live. Supersedes `prerenderedCardSearchComponent`.
searchResultsComponent: typeof GlimmerComponent<SearchResultsComponentSignature>;
Expand Down
2 changes: 1 addition & 1 deletion packages/base/components/card-list.gts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default class CardList extends Component<Signature> {
@consume(CardContextName)
declare cardContext: CardContext | undefined;

// The `search-entry`-rooted query, adapted from the incoming `Query`.
// The `entry`-rooted query, adapted from the incoming `Query`.
// The default fieldset (no `fields` member) resolves to "html, falling back
// to the `item` serialization where no rendering matched" — exactly what the
// grid wants (prerendered HTML for cards; an `item`/`icon` fallback for file
Expand Down
2 changes: 1 addition & 1 deletion packages/boxel-cli/src/commands/realm/ingest-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ class RealmCardIngester extends RealmSyncBase {
// search returns nothing for it — which is why instances and Specs went
// uncopied (the module crawl survives because it uses direct file fetches).
// The realm's own endpoint sees its full index. The request is data-only
// (`fields[search-entry]=item`); the response is a search-entry document
// (`fields[entry]=item`); the response is an entry document
// whose matched `item` serializations resolve out of `included` uniformly
// for normal and published realms (the v1 `data`-vs-`included` split
// disappears — every match is an entry that references its item).
Expand Down
22 changes: 11 additions & 11 deletions packages/boxel-cli/src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface SearchCommandOptions {
profileManager?: ProfileManager;
}

// `_federated-search` speaks the search-entry wire grammar: one query
// rooted on `search-entry`, where entry membership is addressed through
// `_federated-search` speaks the entry wire grammar: one query
// rooted on `entry`, where entry membership is addressed through
// `item.` (the card/file serialization). The type anchor is `item.on` and the
// field paths inside the filter operators carry the `item.` prefix. Callers
// here author ordinary card-rooted queries, so these helpers rewrite a query
Expand Down Expand Up @@ -72,7 +72,7 @@ function toItemFilter(
out.matches = value;
} else {
throw new Error(
`cannot translate filter member "${key}" to a search-entry query — the type anchor is "on"/"type" and field paths live under the ${FIELD_KEYED_OPERATORS.join('/')} operators`,
`cannot translate filter member "${key}" to an entry query — the type anchor is "on"/"type" and field paths live under the ${FIELD_KEYED_OPERATORS.join('/')} operators`,
);
}
}
Expand Down Expand Up @@ -102,15 +102,15 @@ interface SearchEntryRequestBody {
realms?: string[];
// boxel-cli never renders HTML, so it requests the data-only fieldset: each
// entry carries only its full `item` serialization (no prerendered `html`).
fields: { 'search-entry': ['item'] };
fields: { entry: ['item'] };
filter?: Record<string, unknown>;
sort?: Record<string, unknown>[];
page?: unknown;
cardUrls?: unknown;
}

/**
* Build a search-entry request body from a card-rooted query: the
* Build an entry request body from a card-rooted query: the
* `item.`-addressed filter/sort plus the data-only fieldset. Pass `realms` for
* the federated `_federated-search`; omit it to query a single realm's own
* `_search`.
Expand All @@ -120,7 +120,7 @@ export function searchEntryRequestBody(
realms?: string[],
): SearchEntryRequestBody {
let body: SearchEntryRequestBody = {
fields: { 'search-entry': ['item'] },
fields: { entry: ['item'] },
};
if (realms !== undefined) {
body.realms = realms;
Expand Down Expand Up @@ -152,10 +152,10 @@ export function searchEntryRequestBody(
return body;
}

// A data-only search-entry document, narrowed to the shape this client reads:
// A data-only entry document, narrowed to the shape this client reads:
// each entry links its serialization through `item`, and the `card`/`file-meta`
// resource itself travels in `included`. A structural local type rather than
// runtime-common's `SearchEntryCollectionDocument` — that one transitively
// runtime-common's `EntryCollectionDocument` — that one transitively
// pulls the index's `https://cardstack.com/base/*` imports, which don't resolve
// in a plain Node CLI (the same boundary the query helpers above note).
interface SearchEntryDoc {
Expand All @@ -168,7 +168,7 @@ interface SearchEntryDoc {
}

/**
* Flatten a data-only search-entry document into the `item` serializations, in
* Flatten a data-only entry document into the `item` serializations, in
* result order — the same `card`/`file-meta` resources the legacy endpoint
* returned as its top-level `data`. Each entry points at its serialization in
* `included`; resolve and collect them.
Expand Down Expand Up @@ -203,8 +203,8 @@ export function itemsFromSearchEntryDoc(
* Federated search across one or more realms via the `_federated-search`
* server endpoint.
*
* Sends the search-entry-rooted query as a QUERY request requesting the
* data-only fieldset (`fields[search-entry]=item`), and returns the `item`
* Sends the entry-rooted query as a QUERY request requesting the
* data-only fieldset (`fields[entry]=item`), and returns the `item`
* serializations the endpoint links in `included` — the `card`/`file-meta`
* resources callers consume. Uses the server JWT via
* `ProfileManager.authedRealmServerFetch`.
Expand Down
4 changes: 2 additions & 2 deletions packages/boxel-cli/tests/commands/ingest-card-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function makeFakeAuthenticator(fetchedUrls: string[]): RealmAuthenticator {
// The cards the source realm matches for the two shapes the ingester issues:
// instances of the entry card's exported classes, and all base-realm Spec
// cards (filtered by specType + ref in the ingester itself). The type anchor
// arrives `item.`-addressed (`filter['item.on']`) — the search-entry grammar
// arrives `item.`-addressed (`filter['item.on']`) — the entry grammar
// `_search` speaks.
function fakeSearchData(
bodyStr: string,
Expand Down Expand Up @@ -204,7 +204,7 @@ function fakeSearchData(
return [];
}

// Wrap matched cards as a data-only search-entry document — one entry per card
// Wrap matched cards as a data-only entry document — one entry per card
// linking its `item`, with the card resources themselves in `included` (the
// shape `_search` returns; a published realm carries its matches the same
// way, so the ingester needs no published-vs-normal special-casing).
Expand Down
6 changes: 3 additions & 3 deletions packages/boxel-cli/tests/commands/search-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ const CardDefRef = {
name: 'CardDef',
};

describe('searchEntryRequestBody — card-rooted query → search-entry wire grammar', () => {
describe('searchEntryRequestBody — card-rooted query → entry wire grammar', () => {
it('always requests the data-only fieldset and the given realms', () => {
let body = searchEntryRequestBody({}, ['https://realm/a/']);
expect(body).toEqual({
realms: ['https://realm/a/'],
fields: { 'search-entry': ['item'] },
fields: { entry: ['item'] },
});
});

Expand Down Expand Up @@ -105,7 +105,7 @@ describe('searchEntryRequestBody — card-rooted query → search-entry wire gra
});
});

describe('itemsFromSearchEntryDoc — flatten a data-only search-entry doc to items', () => {
describe('itemsFromSearchEntryDoc — flatten a data-only entry doc to items', () => {
it("resolves each entry's item from included, in entry order", () => {
let doc = {
data: [
Expand Down
2 changes: 1 addition & 1 deletion packages/experiments-realm/app-card.gts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class DefaultTabTemplate extends GlimmerComponent<DefaultTabSignature> {
} as Query;
}

// The `search-entry`-rooted query, adapted from the `query` above.
// The `entry`-rooted query, adapted from the `query` above.
// `fitted` is the default rendering, so no `htmlQuery` binding is needed.
// Undefined (no active tab ref) leaves the search component idle.
get searchResultsQuery(): SearchEntryWireQuery | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/experiments-realm/components/card-list.gts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface CardListSignature {
Element: HTMLElement;
}
export class CardList extends GlimmerComponent<CardListSignature> {
// The `search-entry`-rooted query, adapted from the incoming `Query`.
// The `entry`-rooted query, adapted from the incoming `Query`.
// `embedded` is bound through the query's `htmlQuery` field (the way to
// select a prerendered format); a bare `eq.format` would be read as an
// `item.` field path and rejected.
Expand Down
12 changes: 6 additions & 6 deletions packages/host/app/commands/sync-openrouter-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { service } from '@ember/service';

import {
SupportedMimeType,
isSearchEntryCollectionDocument,
isEntryCollectionDocument,
rri,
searchEntryWireQueryFromQuery,
} from '@cardstack/runtime-common';
Expand Down Expand Up @@ -339,8 +339,8 @@ export default class SyncOpenRouterModelsCommand extends HostBaseCommand<
let slugs = new Set<string>();
try {
// Listing existing cards only needs each one's id, so ask the
// search-entry engine for a data-only projection
// (`fields[search-entry]=item`): every entry carries its `item`
// entry engine for a data-only projection
// (`fields[entry]=item`): every entry carries its `item`
// serialization, no prerendered HTML.
let wireQuery = searchEntryWireQueryFromQuery(
{
Expand All @@ -367,9 +367,9 @@ export default class SyncOpenRouterModelsCommand extends HostBaseCommand<

if (response.ok) {
let result = await response.json();
if (isSearchEntryCollectionDocument(result)) {
if (isEntryCollectionDocument(result)) {
for (let entry of result.data) {
// A `search-entry` resource's id is the card URL.
// An `entry` resource's id is the card URL.
let id = entry.id ?? '';
// Extract slug from URL: .../OpenRouterModel/slug-name or .../OpenRouterModel/slug-name.json
let match = id.match(/OpenRouterModel\/([^/]+)$/);
Expand All @@ -382,7 +382,7 @@ export default class SyncOpenRouterModelsCommand extends HostBaseCommand<
}
}
} else {
// A 200 that isn't a search-entry document is unexpected for
// A 200 that isn't an entry document is unexpected for
// /_search; surface it rather than silently treating every model
// as new (same best-effort fallback as the catch below).
console.warn(
Expand Down
6 changes: 3 additions & 3 deletions packages/host/app/components/card-search/panel-content.gts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ interface Signature {
// The results pane of the search sheet / card chooser. Renders through the
// `<SearchResults>` component family: one instance for the realm search, a
// nested one for recents (with the live-recents fallback layered in), then hands
// their yielded `search-entry` streams to `<SheetResults>`, which lays them out
// their yielded `entry` streams to `<SheetResults>`, which lays them out
// into realm / recents / URL-paste sections (with the header, multiselect, the
// Adorn treatment, pagination, and the result count). Resources are
// construct-once: the two `<SearchResults>` own their live-search resources
Expand Down Expand Up @@ -185,7 +185,7 @@ export default class PanelContent extends Component<Signature> {
return this.cardResource?.isLoaded ?? false;
}

// The `search-entry` query for the main realm search, built from the
// The `entry` query for the main realm search, built from the
// shared `Query` builder via `searchEntryWireQueryFromQuery`. Fitted is the
// default rendering, so no `htmlQuery` override is needed in the default
// variant; the mini variant pins it to the uniform CardDef fitted tile
Expand Down Expand Up @@ -224,7 +224,7 @@ export default class PanelContent extends Component<Signature> {
// rendering through the wire filter's top-level `eq` htmlQuery — fitted
// format at the CardDef render type, served from the per-ancestor
// `fitted_html` entries the index already carries. The `eq` carries only the
// htmlQuery binding, which the search-entry engine lifts out and then
// htmlQuery binding, which the entry engine lifts out and then
// dissolves the now-empty `eq`, so the rest of the filter is untouched;
// `buildSearchQuery`/`buildRecentsQuery` never emit a top-level `eq`, so
// there is nothing to collide with. Non-mini variants pass through unchanged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ interface Signature {

// One section of the search-results pane — a realm group, the URL-paste row, or
// the recents row. Lays its rows out into a grid of `ResultTile`s; each tile
// renders through the search-entry rendering surface (a search-entry's
// renders through the entry rendering surface (an entry's
// `entry.component`, or a live `CardDef` for the URL paste / live-recents
// fallback).
export default class ResultSection extends Component<Signature> {
Expand Down
4 changes: 2 additions & 2 deletions packages/host/app/components/card-search/result-tile.gts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default class SearchResultTile extends Component<Signature> {
return this.args.newCard != null;
}

// The type name shown in the Adorn type-label tab. Search-entry rows carry it
// The type name shown in the Adorn type-label tab. Entry rows carry it
// on their deduped `icon` resource (no live instance needed); the URL-paste
// live card supplies it in-memory. The "Create New" row has no label.
private get adornTypeName(): string | undefined {
Expand All @@ -127,7 +127,7 @@ export default class SearchResultTile extends Component<Signature> {
return undefined;
}

// Type-name precedence mirrored for the icon: search-entry rows carry icon
// Type-name precedence mirrored for the icon: entry rows carry icon
// HTML on the `icon` resource; the live card supplies a component in-memory.
private get adornTypeIcon(): unknown {
if (!this.args.adorn || this.isNewCard) return undefined;
Expand Down
9 changes: 3 additions & 6 deletions packages/host/app/components/card-search/search-results.gts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { HydrationMode } from './hydratable-card';

import type StoreService from '../../services/store';

// The one search component family. Consumes the heterogeneous `search-entry`
// The one search component family. Consumes the heterogeneous `entry`
// stream from `getSearchEntriesResource` (through the shared render-stable
// view-model layer) and renders it transparently — prerendered HTML inert (the
// fast path, hydrated lazily on interaction) or the live serialization. Used
Expand Down Expand Up @@ -60,7 +60,7 @@ export default class SearchResults extends Component<SearchResultsComponentSigna

// Selective Store inflate: deposit only full `item` serializations so a
// by-URL read (or the hydration GET) resolves without a round-trip. Sparse
// items and `search-entry`s are never deposited (the store method no-ops on a
// items and `entry`s are never deposited (the store method no-ops on a
// sparse item); an item carrying an error doc is skipped here too — it stands
// in for a card that failed to render and must not enter the Store. A
// render-side effect keyed on the live entry set, so it deposits an
Expand All @@ -75,10 +75,7 @@ export default class SearchResults extends Component<SearchResultsComponentSigna
this.store
.inflateSearchEntryItem(entry.item)
.catch((err: unknown) => {
this.#log.error(
`failed to inflate search-entry item ${entry.id}`,
err,
);
this.#log.error(`failed to inflate entry item ${entry.id}`, err);
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/host/app/components/card-search/sheet-results.gts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ interface Signature {
Blocks: {};
}

// Lays the heterogeneous `search-entry` stream from `<SearchResults>` out into
// Lays the heterogeneous `entry` stream from `<SearchResults>` out into
// the search sheet's realm / recents / URL-paste sections, with the header,
// multiselect, the Adorn treatment, pagination, and the result count expressed
// here at the call site over the yielded entries. Every derivation is a getter
Expand Down
Loading
Loading