From 605bf142b41e2bd68f88bf2c27114c47e4f4ea7b Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Fri, 17 Jul 2026 16:20:16 +0800 Subject: [PATCH 1/3] Add supportingCards to listings: installed with examples, hidden from detail view Query-based examples find their data by query, not linksTo, so the relationship walk in install/submission can never discover it. The new supportingCards field declares those cards explicitly. They flow through create, install (and therefore remix), use, and submission identically to examples; the only difference is the catalog detail page doesn't render them. Listing creation now classifies open cards by type: instances of the listed codeRef become examples, everything else becomes supportingCards. Co-Authored-By: Claude Fable 5 --- catalog-app/listing/listing.gts | 2 + commands/collect-submission-files.ts | 9 ++- commands/listing-create.ts | 85 +++++++++++++++++++++++++--- commands/listing-install.ts | 19 +++++-- commands/listing-use.ts | 21 ++++--- 5 files changed, 108 insertions(+), 28 deletions(-) diff --git a/catalog-app/listing/listing.gts b/catalog-app/listing/listing.gts index ed518456..52d22084 100644 --- a/catalog-app/listing/listing.gts +++ b/catalog-app/listing/listing.gts @@ -934,6 +934,8 @@ export class Listing extends CardDef { @field license = linksTo(() => License, { searchable: true }); @field images = linksToMany(ImageDef, { searchable: true }); @field examples = linksToMany(() => CardDef); + // installed alongside examples but never rendered in the listing detail view + @field supportingCards = linksToMany(() => CardDef); @field skills = linksToMany(() => Skill); @field cardTitle = contains(StringField, { diff --git a/commands/collect-submission-files.ts b/commands/collect-submission-files.ts index e50890ef..1ff8476c 100644 --- a/commands/collect-submission-files.ts +++ b/commands/collect-submission-files.ts @@ -109,10 +109,13 @@ export default class CollectSubmissionFilesCommand extends Command< ); } - let examplesToSnapshot = listing.examples; + let examplesToSnapshot = [ + ...(listing.examples ?? []), + ...(listing.supportingCards ?? []), + ]; let fileDefUrls = new Set(); - if (listing.examples?.length) { - let expanded = await this.expandInstances(listing.examples); + if (examplesToSnapshot.length) { + let expanded = await this.expandInstances(examplesToSnapshot); examplesToSnapshot = expanded.instances; fileDefUrls = expanded.fileDefUrls; } diff --git a/commands/listing-create.ts b/commands/listing-create.ts index c72361cc..51869c28 100644 --- a/commands/listing-create.ts +++ b/commands/listing-create.ts @@ -118,12 +118,18 @@ export default class ListingCreateCommand extends Command< const displayName = (cardDef as any)?.displayName ?? codeRef.name; const catalogRealm = await this.getCatalogRealm(); + const { exampleCardIds, supportingCardIds } = await this.classifyOpenCards( + openCardIds, + codeRef, + ); + let relationships: Record = {}; - if (openCardIds && openCardIds.length > 0) { - openCardIds.forEach((id, index) => { - relationships[`examples.${index}`] = { links: { self: id } }; - }); - } + exampleCardIds.forEach((id, index) => { + relationships[`examples.${index}`] = { links: { self: id } }; + }); + supportingCardIds.forEach((id, index) => { + relationships[`supportingCards.${index}`] = { links: { self: id } }; + }); const listingDoc: LooseSingleCardDocument = { data: { @@ -144,12 +150,12 @@ export default class ListingCreateCommand extends Command< const commandModule = await loadCommandModule(this.commandContext); const listingCard = listing as CardAPI.CardDef; - const firstOpenCardId = openCardIds?.[0]; + const firstExampleCardId = exampleCardIds[0]; const examplePromise = this.autoLinkExample( listingCard, codeRef, - openCardIds, + exampleCardIds, ); const backgroundTasks = [ @@ -190,7 +196,7 @@ export default class ListingCreateCommand extends Command< promise: this.linkSpecs( listingCard, targetRealm, - firstOpenCardId ?? codeRef?.module, + firstExampleCardId ?? codeRef?.module, codeRef.module, codeRef, ), @@ -216,6 +222,69 @@ export default class ListingCreateCommand extends Command< return result; } + // Open cards that are instances of the listed type become examples; other + // open cards (e.g. data a query-based example depends on) become + // supportingCards, which install but don't render in the listing detail. + private async classifyOpenCards( + openCardIds: string[] | undefined, + codeRef: ResolvedCodeRef, + ): Promise<{ exampleCardIds: string[]; supportingCardIds: string[] }> { + const classified = await Promise.all( + (openCardIds ?? []).map(async (id) => { + try { + const instance = await new GetCardCommand( + this.commandContext, + ).execute({ cardId: id }); + return { + id, + isExample: + isCardInstance(instance) && + this.isInstanceOfCodeRef(instance as CardAPI.CardDef, codeRef), + }; + } catch { + return { id, isExample: true }; + } + }), + ); + return { + exampleCardIds: classified.filter((c) => c.isExample).map((c) => c.id), + supportingCardIds: classified + .filter((c) => !c.isExample) + .map((c) => c.id), + }; + } + + private isInstanceOfCodeRef( + instance: CardAPI.CardDef, + codeRef: ResolvedCodeRef, + ): boolean { + let targetModule: string; + try { + targetModule = trimExecutableExtension(rri(new URL(codeRef.module).href)); + } catch { + return false; + } + let current: CardAPI.BaseDefConstructor | undefined = + instance.constructor as CardAPI.BaseDefConstructor; + while (current) { + const ref = identifyCard(current); + if (ref && !('type' in ref) && ref.name === codeRef.name) { + try { + if ( + trimExecutableExtension(rri(new URL(ref.module).href)) === + targetModule + ) { + return true; + } + } catch { + // unresolvable ancestor module; keep walking + } + } + current = getAncestor(current) ?? undefined; + } + return false; + } + private async guessListingType( codeRef: ResolvedCodeRef, cardDef: CardAPI.BaseDefConstructor | undefined, diff --git a/commands/listing-install.ts b/commands/listing-install.ts index 1deea2d8..efa35c99 100644 --- a/commands/listing-install.ts +++ b/commands/listing-install.ts @@ -98,9 +98,14 @@ export default class ListingInstallCommand extends Command< // this is intentionally to type because base command cannot interpret Listing type from catalog const listing = listingInput as Listing; - let examplesToInstall = listing.examples; - if (listing.examples?.length) { - examplesToInstall = await this.expandInstances(listing.examples); + // seed examples first so the primary example stays the first planned instance + let hasPrimaryExamples = (listing.examples?.length ?? 0) > 0; + let examplesToInstall = [ + ...(listing.examples ?? []), + ...(listing.supportingCards ?? []), + ]; + if (examplesToInstall.length) { + examplesToInstall = await this.expandInstances(examplesToInstall); } // side-effects @@ -125,9 +130,11 @@ export default class ListingInstallCommand extends Command< resolver, virtualNetwork, ); - let firstInstance = r.instancesCopy[0]; - exampleCardId = join(realmUrl, firstInstance.lid); - selectedCodeRef = firstInstance.targetCodeRef; + if (hasPrimaryExamples) { + let firstInstance = r.instancesCopy[0]; + exampleCardId = join(realmUrl, firstInstance.lid); + selectedCodeRef = firstInstance.targetCodeRef; + } return r; }) .addIf(listing.skills?.length > 0, (resolver: ListingPathResolver) => { diff --git a/commands/listing-use.ts b/commands/listing-use.ts index def8b1a5..e3ea5a9d 100644 --- a/commands/listing-use.ts +++ b/commands/listing-use.ts @@ -78,17 +78,16 @@ export default class ListingUseCommand extends Command< }); } - if (listing.examples) { - const sourceCards = (listing.examples as CardAPI.CardDef[]).map( - (example) => example, - ); - for (const card of sourceCards) { - await new CopyCardToRealmCommand(this.commandContext).execute({ - sourceCard: card, - targetRealm: realmUrl, - localDir, - }); - } + const sourceCards = [ + ...((listing.examples ?? []) as CardAPI.CardDef[]), + ...((listing.supportingCards ?? []) as CardAPI.CardDef[]), + ]; + for (const card of sourceCards) { + await new CopyCardToRealmCommand(this.commandContext).execute({ + sourceCard: card, + targetRealm: realmUrl, + localDir, + }); } if ('skills' in listing && Array.isArray(listing.skills)) { From 3e65048aa35179fbe7b5502f6f900e21379e9403 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Fri, 17 Jul 2026 17:01:46 +0800 Subject: [PATCH 2/3] listing-create: honor explicit supportingCardIds from the input Callers (e.g. the host create-listing modal) can now pass supporting cards explicitly instead of relying on type classification. Explicit ids are taken verbatim; a card also selected as an example stays an example. Co-Authored-By: Claude Fable 5 --- commands/listing-create.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/commands/listing-create.ts b/commands/listing-create.ts index 51869c28..8f0f7ed8 100644 --- a/commands/listing-create.ts +++ b/commands/listing-create.ts @@ -96,6 +96,9 @@ export default class ListingCreateCommand extends Command< input: BaseCommandModule.ListingCreateInput, ): Promise { let { openCardIds, codeRef, targetRealm } = input; + // tolerate a base ListingCreateInput that predates supportingCardIds + let explicitSupportingCardIds = (input as { supportingCardIds?: string[] }) + .supportingCardIds; if (!codeRef) { throw new Error('codeRef is required'); @@ -118,10 +121,16 @@ export default class ListingCreateCommand extends Command< const displayName = (cardDef as any)?.displayName ?? codeRef.name; const catalogRealm = await this.getCatalogRealm(); - const { exampleCardIds, supportingCardIds } = await this.classifyOpenCards( - openCardIds, - codeRef, - ); + const classified = await this.classifyOpenCards(openCardIds, codeRef); + const exampleCardIds = classified.exampleCardIds; + // explicit supporting cards are taken verbatim (no type check); a card + // also picked as an example stays an example + const supportingCardIds = [ + ...new Set([ + ...classified.supportingCardIds, + ...(explicitSupportingCardIds ?? []), + ]), + ].filter((id) => !exampleCardIds.includes(id)); let relationships: Record = {}; exampleCardIds.forEach((id, index) => { From bea1e78f05354c0364962dc0edbd82da991a1b2d Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Fri, 17 Jul 2026 18:38:48 +0800 Subject: [PATCH 3/3] listing-use: dedupe example and supporting cards before copying A card linked as both an example and a supporting card was copied twice into the install folder. Co-Authored-By: Claude Fable 5 --- commands/listing-use.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/commands/listing-use.ts b/commands/listing-use.ts index e3ea5a9d..8b306b53 100644 --- a/commands/listing-use.ts +++ b/commands/listing-use.ts @@ -79,8 +79,12 @@ export default class ListingUseCommand extends Command< } const sourceCards = [ - ...((listing.examples ?? []) as CardAPI.CardDef[]), - ...((listing.supportingCards ?? []) as CardAPI.CardDef[]), + ...new Map( + [ + ...((listing.examples ?? []) as CardAPI.CardDef[]), + ...((listing.supportingCards ?? []) as CardAPI.CardDef[]), + ].map((card) => [card.id ?? card, card]), + ).values(), ]; for (const card of sourceCards) { await new CopyCardToRealmCommand(this.commandContext).execute({