@nuanu-ai/agentify-contracts 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/card.js CHANGED
@@ -25,8 +25,9 @@
25
25
  * more shape to keep in step for no gain to anybody.
26
26
  */
27
27
  import { z } from "zod";
28
- import { ParamSpecSchema, paramSpecToValidator } from "./param-spec.js";
29
- import { IdentifierSchema, MoneySchema, TimestampSchema } from "./primitives.js";
28
+ import { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, paramSpecToValidator, } from "./param-spec.js";
29
+ import { notPlainTextIn } from "./plain-text.js";
30
+ import { IdentifierSchema, MoneySchema, OpenWordSchema, TimestampSchema } from "./primitives.js";
30
31
  import { SellingStateSchema } from "./selling.js";
31
32
  /**
32
33
  * When the product reaches the agent, and therefore when the money moves.
@@ -117,8 +118,86 @@ const listedText = (what) => z
117
118
  * holds it is the merchants table, so this schema is the rule alone, applied
118
119
  * wherever a merchant's listing name is written down.
119
120
  */
120
- export const ServiceNameSchema = listedText("service name").meta({
121
- description: "The name a seller is listed under in a discovery catalog. At most 32 characters of printable ASCII, because that is what the catalog carries; a name outside that is refused here rather than truncated there, where nobody would be told.",
121
+ export const ServiceNameSchema = listedText("service name")
122
+ .superRefine((name, ctx) => {
123
+ // Every agent reads the name on every card and order of the merchant's
124
+ // (ADR-0034), so the door holds it to the card's own plain-text rule
125
+ // (ADR-0017): markup and character references are refused and named,
126
+ // not cleaned into words the merchant never wrote.
127
+ for (const phrase of notPlainTextIn(name, "one line")) {
128
+ ctx.addIssue({
129
+ code: "custom",
130
+ message: `this service name carries ${phrase}, and a service name is plain text, which an agent reads exactly as it is written`,
131
+ });
132
+ }
133
+ })
134
+ .meta({
135
+ description: "The name a seller is listed under in a discovery catalog and shown to every agent beside their products. At most 32 characters of printable ASCII, because that is what the catalog carries; a name outside that is refused here rather than truncated there, where nobody would be told. It is plain text: HTML markup and character references such as & are refused, and an ampersand written as text passes.",
136
+ });
137
+ /**
138
+ * The name as it is stored and read back: every rule the door holds it to
139
+ * except that it is plain text.
140
+ *
141
+ * That rule belongs to the door and not to the reader, for the reason the
142
+ * card's own words give (ADR-0017): a name stored before the rule may carry
143
+ * markup, every answer is held to its contract on the way out, and one old row
144
+ * would fail every card and order of its merchant's instead of being put right
145
+ * the next time the merchant sets their name.
146
+ */
147
+ const StoredServiceNameSchema = listedText("service name");
148
+ /** What a seller's site is held to, said once for the refusal and the description. */
149
+ const SITE_FORM = "a seller's site is https:// and the domain name of their shop, with nothing after it — https://shop.example: in lower case, a domain name rather than an IP address or a single word, with no path, query, fragment, port, credentials or trailing slash";
150
+ /**
151
+ * The address of a seller's own shop on the web, where an agent takes what an
152
+ * order cannot answer (ADR-0034).
153
+ *
154
+ * Only an https origin, because anything after the host — a path, a query, a
155
+ * fragment — would be text of the merchant's own reaching every agent that
156
+ * reads the card, which is the free text the decision refuses. The host is the
157
+ * one part the merchant writes, so it is held to a domain name too: labels of
158
+ * letters, digits and hyphens, at most 253 characters, ending in a zone of
159
+ * letters or a punycode one. Anything a URL parser keeps as written would
160
+ * otherwise pass — a sentence of instructions, a quote, a host of any length —
161
+ * and so would a single word or an IP address, which point other people's
162
+ * agents into somebody's own network. What the pattern cannot do is tell a
163
+ * public name from a private one: a name in a zone kept for local networks,
164
+ * or a public one whose address leads into one, passes, and so does a
165
+ * hyphenated sentence of up to 253 characters that is a valid name. That is
166
+ * why an agent is told, beside every site, that nobody checked it.
167
+ *
168
+ * The pattern says all that in a form the JSON Schema export keeps, and it
169
+ * stops the check where it fails, so an address copied with a slash at the end
170
+ * is told once. Asking the URL parser for the origin catches what the pattern
171
+ * leaves, such as a punycode label the parser would write differently.
172
+ */
173
+ export const SellerSiteSchema = z
174
+ .string()
175
+ .regex(/^https:\/\/(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$/, { message: SITE_FORM, abort: true })
176
+ .refine((site) => {
177
+ try {
178
+ return new URL(site).origin === site;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }, SITE_FORM)
184
+ .meta({ description: `The https origin of a seller's own shop: ${SITE_FORM}.` });
185
+ /**
186
+ * Who sells, as an agent reads it beside a product and on an order: the name
187
+ * the merchant sells under and the site of their shop (ADR-0034).
188
+ *
189
+ * Both are the merchant's own word and nothing here checked either, which the
190
+ * description says to the reader who acts on it — a name and a site can be
191
+ * anybody's. Null is "none given": a merchant who gave no site has none here,
192
+ * and a name is missing only from an order whose merchant has since lost it.
193
+ */
194
+ export const SellerSchema = z
195
+ .strictObject({
196
+ name: StoredServiceNameSchema.nullable(),
197
+ site: SellerSiteSchema.nullable(),
198
+ })
199
+ .meta({
200
+ description: "Who sells: the name the merchant sells under and the https address of their shop's own site, both as the merchant gave them. Agentify did not check either, so a name and a site can be anybody's. The site is where to take what an order cannot answer — a parcel that did not arrive, a return, the shop's terms. Either is null where the merchant has given none.",
122
201
  });
123
202
  /**
124
203
  * The words a merchant puts on one product so an agent searching a catalog can
@@ -219,19 +298,31 @@ const DescriptionSchema = z
219
298
  * JSON Schema, so the same constraint goes into the metadata as
220
299
  * `minProperties`, where a generator can still see it.
221
300
  *
222
- * One schema, used by the published card and by the projection below, because
301
+ * One rule, held by the published card and by the projection below, because
223
302
  * the promise is the same one: what the agent reads before paying is what the
224
303
  * merchant is held to afterwards. Two copies of a refinement would be two
225
- * promises, and the export would carry whichever was edited last.
304
+ * promises, and the export would carry whichever was edited last. The two
305
+ * declarations it is put on differ only in what else a field may carry: the
306
+ * merchant's is closed, and the agent's takes what is added later
307
+ * (ADR-0006 §5).
226
308
  */
227
- const DeclaredResultSchema = ParamSpecSchema.refine(
309
+ const declaringAResult = (declaration) => declaration
310
+ .refine(
228
311
  // Not merely non-empty: at least one field that actually arrives. A
229
312
  // declaration of one field marked `required: false` satisfies "at least
230
313
  // one" and promises the agent exactly as much as an empty one does.
231
- (spec) => Object.values(spec).some((field) => field.required !== false), "a card declares at least one field of what the agent receives, and at least one of them arrives every time").meta({
314
+ (spec) => Object.values(spec).some((field) => field.required !== false), "a card declares at least one field of what the agent receives, and at least one of them arrives every time")
315
+ .meta({
232
316
  description: "What the agent receives on delivery. At least one field, and at least one of the fields is not marked required: false — a result that might be entirely absent promises nothing.",
233
317
  minProperties: 1,
234
318
  });
319
+ const DeclaredResultSchema = declaringAResult(ParamSpecSchema);
320
+ /**
321
+ * A declaration as an agent reads it: the merchant's fields, each open to what
322
+ * is added to a field later (ADR-0006 §5), so a new attribute of one field
323
+ * leaves the card readable to an agent built before it.
324
+ */
325
+ const ReadDeclarationSchema = z.record(ParamNameSchema, FieldSpecSchema.loose());
235
326
  /**
236
327
  * The short forms a card may be written in, and the one form they all become.
237
328
  *
@@ -348,8 +439,14 @@ const CardFieldsSchema = z.strictObject({
348
439
  *
349
440
  * Written as the two fields, or as the one string that becomes them. What is
350
441
  * stored and what an agent reads are the two fields either way.
442
+ *
443
+ * Publishing holds it to the price rule in `price-rule.ts`, outside this
444
+ * schema so that a card stored before the rule stays readable; the export
445
+ * says so, or a generated client would first meet the rule as a refusal.
351
446
  */
352
- price: CardPriceSchema,
447
+ price: CardPriceSchema.meta({
448
+ description: 'Publishing refuses a price that is zero, not in USD or USDC, or written with fewer than two or more than six digits after the dot ("5.00", "0.001"), so that a price the catalog shows is one a payment can be taken at.',
449
+ }),
353
450
  /** What the agent has to supply to buy. Absent when the purchase needs no input. */
354
451
  params: writtenShort(ParamSpecSchema).optional(),
355
452
  result: writtenShort(DeclaredResultSchema),
@@ -390,7 +487,7 @@ const CardFieldsSchema = z.strictObject({
390
487
  * wait for a synchronous answer is our system-wide budget, the same for every
391
488
  * product, so it has no field here at all.
392
489
  */
393
- export const CardSchema = CardFieldsSchema.superRefine((card, ctx) => {
490
+ const cardRules = (card, ctx) => {
394
491
  if (card.fulfillment === "confirm") {
395
492
  // The gate, and the reason it is a refusal rather than a note somewhere.
396
493
  // A confirmation request reaches the merchant before any money moves and
@@ -428,13 +525,106 @@ export const CardSchema = CardFieldsSchema.superRefine((card, ctx) => {
428
525
  message: 'a synchronous card delivers inside the system-wide response budget and sets no delivery deadline; "async" and "confirm" do',
429
526
  });
430
527
  }
431
- }).meta({
432
- // JSON Schema has no way to say "this field only when that one has this
433
- // value", and zod drops the rules above when it renders a document. Left at
434
- // that, an engineer generating a client from the export would build one that
435
- // sends deadlines a card cannot carry and only find out on the first publish.
436
- // Saying it in words is weaker than checking it, and better than silence.
437
- description: 'A product in the catalog, as the merchant publishes it. Three rules are enforced beyond the shape below. A card cannot be published as fulfillment "confirm" during the pilot: the confirmation request has no shape on the wire yet, so a handler could not tell one from a paid order. confirm_deadline_seconds is only allowed when fulfillment is "confirm", and fulfill_deadline_seconds only when fulfillment is "async" or "confirm" — a synchronous card delivers inside the system-wide response budget and names no deadline of its own. This document describes the card as it is stored and read back; three fields also take a shorter spelling at publication, which JSON Schema has no way to show alongside the canonical one. price may be written as one string, the amount and the currency code with a single space between them ("5.00 USD"). A field of params or result may be written as its type word alone (access_url: "string") where it carries no title and no required flag. fulfillment may be left out, and a card that leaves it out is "sync". Each of those is opened out into the form below as the card is accepted, so a card generated from this document is accepted unchanged and a card read back is always in this form.',
528
+ };
529
+ // JSON Schema has no way to say "this field only when that one has this
530
+ // value", and zod drops the rules above when it renders a document. Left at
531
+ // that, an engineer generating a client from the export would build one that
532
+ // sends deadlines a card cannot carry and only find out on the first publish.
533
+ // Saying it in words is weaker than checking it, and better than silence.
534
+ const CARD_RULES_IN_WORDS = 'A card cannot be published as fulfillment "confirm" during the pilot: the confirmation request has no shape on the wire yet, so a handler could not tell one from a paid order. confirm_deadline_seconds is only allowed when fulfillment is "confirm", and fulfill_deadline_seconds only when fulfillment is "async" or "confirm" — a synchronous card delivers inside the system-wide response budget and names no deadline of its own.';
535
+ /**
536
+ * The words on a card that an agent reads: the title, the description, and
537
+ * the title of each field the card declares.
538
+ *
539
+ * It reads whatever arrived rather than a card known to be whole, because the
540
+ * rule runs beside the shape checks and not after them — a merchant told about
541
+ * a price first and about their markup on the next attempt fixes one thing per
542
+ * publish. So anything that is not where a card keeps its words is passed over
543
+ * here and named by the shape check instead.
544
+ */
545
+ const wordsOf = (card) => {
546
+ if (!isRecord(card))
547
+ return [];
548
+ const words = [];
549
+ if (typeof card.title === "string") {
550
+ words.push({ path: ["title"], text: card.title, called: "title", lines: "one line" });
551
+ }
552
+ if (typeof card.description === "string") {
553
+ words.push({
554
+ path: ["description"],
555
+ text: card.description,
556
+ called: "description",
557
+ lines: "several lines",
558
+ });
559
+ }
560
+ for (const declaration of ["params", "result"]) {
561
+ const fields = card[declaration];
562
+ if (!isRecord(fields))
563
+ continue;
564
+ for (const [name, field] of Object.entries(fields)) {
565
+ if (isRecord(field) && typeof field.title === "string") {
566
+ words.push({
567
+ path: [declaration, name, "title"],
568
+ text: field.title,
569
+ called: "title",
570
+ lines: "one line",
571
+ });
572
+ }
573
+ }
574
+ }
575
+ return words;
576
+ };
577
+ /**
578
+ * The card's words are plain text, and a finding for each kind of thing in them
579
+ * that is not (see `plain-text.ts` for what counts and why the door refuses
580
+ * rather than cleans).
581
+ *
582
+ * Each finding names the field, what was found in it and where, and what the
583
+ * field is held to. The sentence is written for the merchant who has to find
584
+ * the characters in their own editor, which is why it quotes the fragment and
585
+ * counts the position rather than printing the rule.
586
+ */
587
+ const plainWords = (card, ctx) => {
588
+ for (const words of wordsOf(card)) {
589
+ const heldTo = words.lines === "one line"
590
+ ? `a ${words.called} is plain text on one line`
591
+ : `a ${words.called} is plain text`;
592
+ for (const phrase of notPlainTextIn(words.text, words.lines)) {
593
+ ctx.addIssue({
594
+ code: "custom",
595
+ path: [...words.path],
596
+ message: `this ${words.called} carries ${phrase}, and ${heldTo}, which an agent reads exactly as it is written`,
597
+ });
598
+ }
599
+ }
600
+ };
601
+ const PLAIN_TEXT_IN_WORDS = "The title, the description and the title of every declared field are plain text, which an agent reads exactly as it is written: a card is refused where one of them carries HTML markup (a tag, or the opening of a comment), an HTML character reference such as & or ’, or a control character, and a description alone may carry line feeds. An ampersand, a comparison or an arrow written as text passes.";
602
+ /**
603
+ * The card as it is stored and as its merchant reads it back: every rule a
604
+ * publish holds it to except that its words are plain text.
605
+ *
606
+ * That one rule belongs to the door and not to the reader. A card stored
607
+ * before it may carry markup, and every answer that carries a stored card is
608
+ * held to its contract on the way out — so holding a stored card to the rule
609
+ * would fail the merchant's whole list, and the page of the catalog it sits
610
+ * on, over one old row, and the merchant could not even see the card they were
611
+ * meant to republish.
612
+ */
613
+ const StoredCardSchema = CardFieldsSchema.superRefine(cardRules).meta({
614
+ description: `A product in the catalog, as it is stored and as its merchant reads it back. Three rules hold beyond the shape below. ${CARD_RULES_IN_WORDS} A publish also holds the title, the description and each declared field's title to plain text; reading a card back does not, so a card stored before that rule is read back exactly as it was stored.`,
615
+ });
616
+ /**
617
+ * A card as a merchant publishes it.
618
+ *
619
+ * The plain-text rule runs whatever else is wrong with the card, so a merchant
620
+ * hears about their markup in the same answer as about their price. The rules
621
+ * that compare one field with another run only once the shape is right, as
622
+ * they always have: they need the fields to be what they claim to be.
623
+ */
624
+ export const CardSchema = CardFieldsSchema.superRefine(cardRules)
625
+ .superRefine(plainWords, { when: () => true })
626
+ .meta({
627
+ description: `A product in the catalog, as the merchant publishes it. Four rules are enforced beyond the shape below. ${CARD_RULES_IN_WORDS} ${PLAIN_TEXT_IN_WORDS} The shape below is the canonical card, the form every card is stored and read back in; three fields also take a shorter spelling at publication, which JSON Schema has no way to show alongside the canonical one. price may be written as one string, the amount and the currency code with a single space between them ("5.00 USD"). A field of params or result may be written as its type word alone (access_url: "string") where it carries no title and no required flag. fulfillment may be left out, and a card that leaves it out is "sync". Each of those is opened out into the form below as the card is accepted, so a card generated from this document is accepted unchanged and a card read back is always in this form.`,
438
628
  });
439
629
  /**
440
630
  * The check an agent's purchase parameters are held to, for this card.
@@ -471,11 +661,16 @@ export const deliveryCheckFor = (card) => paramSpecToValidator(card.result, "del
471
661
  * gateway's, and this document does not claim to know it.
472
662
  *
473
663
  * Both deadlines stay, because they are the merchant's promise to the agent
474
- * about how long it may wait, and the agent is told them before it pays. They
475
- * sit on the modes that have them, as branches of a union rather than as a
476
- * rule attached to one object: JSON Schema cannot say "this field only when
477
- * that one has this value", and zod drops such a rule without a word, so as
478
- * branches the constraint crosses whole into the export.
664
+ * about how long it may wait, and the agent is told them before it pays. The
665
+ * projection writes each only on a mode that has it, and the document says so
666
+ * in words rather than as structure: it takes fields added later (ADR-0006
667
+ * §5), so a field an agent does not expect is one it ignores, not one it can
668
+ * refuse.
669
+ *
670
+ * The mode is a word whose known values are listed beside it, for the same
671
+ * reason. The storefront has no version, so a mode added later reaches agents
672
+ * that still hold this contract, and to them it has to be a card to pass over
673
+ * rather than a catalog they cannot read.
479
674
  *
480
675
  * `as_of` is added: the moment the price shown here was published. A price
481
676
  * with no moment behind it cannot be judged stale, and this is the only
@@ -488,27 +683,24 @@ export const deliveryCheckFor = (card) => paramSpecToValidator(card.result, "del
488
683
  * page — so carrying them here would put a foreign catalog's constraint in
489
684
  * front of a reader who has no use for it.
490
685
  *
491
- * One thing an agent might reasonably want is not here, and saying so is
492
- * better than leaving it to be discovered: nothing in this document names who
493
- * is selling. There is a shape in this contract for one name a seller carries —
494
- * `ServiceNameSchema`, the name a discovery catalog lists them under — and it
495
- * is deliberately not this. That name exists to satisfy one channel's rules,
496
- * a merchant may have none, and standing it in for a public identity would
497
- * answer a question — who "we" are to the buyer, and who the merchant is —
498
- * that is still open.
686
+ * Who sells is here as the merchant gave it, a name and the site of their
687
+ * shop (ADR-0034), and as nothing more: no profile, no contact of ours, and no
688
+ * claim that either was checked. Who "we" are to the buyer is a question this
689
+ * document still does not answer.
499
690
  */
500
- const PublicCardFieldsSchema = z.strictObject({
691
+ export const PublicCardSchema = z
692
+ .looseObject({
501
693
  /** Our catalog identifier, the one a purchase, a receipt and a status use. */
502
694
  id: IdentifierSchema,
503
695
  title: TitleSchema,
504
696
  description: DescriptionSchema,
505
697
  /** The price in the catalog: what an agent compares when it is choosing. */
506
- price: MoneySchema,
698
+ price: MoneySchema.loose(),
507
699
  /** When the price above was published. */
508
700
  as_of: TimestampSchema,
509
701
  /** What the agent has to supply to buy. Absent when the purchase needs no input. */
510
- params: ParamSpecSchema.optional(),
511
- result: DeclaredResultSchema,
702
+ params: ReadDeclarationSchema.optional(),
703
+ result: declaringAResult(ReadDeclarationSchema),
512
704
  /**
513
705
  * Whether the merchant is asked for this product's price and availability at
514
706
  * the moment of purchase, so the sale may go through at a price other than
@@ -519,30 +711,31 @@ const PublicCardFieldsSchema = z.strictObject({
519
711
  * about to move, read as true it distrusts a price that never moves.
520
712
  */
521
713
  price_checked_at_purchase: z.boolean(),
522
- });
523
- export const PublicCardSchema = z
524
- .discriminatedUnion("fulfillment", [
525
- // Synchronous: the product arrives in the answer to the purchase, inside our
526
- // own response budget — one number for every product on the platform, which
527
- // is why no card names it and no card may name a delivery deadline instead.
528
- PublicCardFieldsSchema.extend({ fulfillment: z.literal("sync") }),
529
- // Asynchronous: the money moves at the purchase and the product comes later,
530
- // within the merchant's own delivery deadline where they set one.
531
- PublicCardFieldsSchema.extend({
532
- fulfillment: z.literal("async"),
533
- fulfill_deadline_seconds: z.int().positive().optional(),
534
- }),
535
- // With confirmation: the merchant is asked first and the payment follows
536
- // their yes, so both waits exist. No card can be published in this mode
537
- // during the pilot; the branch is here because the mode is in the
538
- // vocabulary, and a branch missing from a projection would be a second gate
539
- // in a second place for whoever lifts the first one.
540
- PublicCardFieldsSchema.extend({
541
- fulfillment: z.literal("confirm"),
542
- confirm_deadline_seconds: z.int().positive().optional(),
543
- fulfill_deadline_seconds: z.int().positive().optional(),
544
- }),
545
- ])
714
+ /**
715
+ * Who sells this product, in the merchant's own words and unchecked. It is
716
+ * read with the catalog rather than copied onto the card at publishing, so
717
+ * a merchant who changes their name or gives a site is found by it on every
718
+ * card they already sell.
719
+ */
720
+ seller: SellerSchema.loose(),
721
+ /**
722
+ * How the product is sold: "sync", "async" or "confirm" today, and a word
723
+ * added later reaches this same document. A reader keeps a default arm,
724
+ * and a card whose mode it does not know is one to pass over.
725
+ *
726
+ * Two branches rather than a bare string, so the known values cross into
727
+ * the exported document as a list a client can switch over, beside the
728
+ * branch that reads any other word (`OpenWordSchema`). A declared field's
729
+ * type, inside params and result, stays a closed list: a card with a type
730
+ * a reader does not know is one it cannot fill in, and it is passed over
731
+ * like a card of an unknown mode.
732
+ */
733
+ fulfillment: z.union([FulfillmentSchema, OpenWordSchema]),
734
+ /** On "async" and "confirm": how long the merchant has to deliver, in seconds. */
735
+ fulfill_deadline_seconds: z.int().positive().optional(),
736
+ /** On "confirm": how long the merchant has to say yes, in seconds. */
737
+ confirm_deadline_seconds: z.int().positive().optional(),
738
+ })
546
739
  .meta({
547
740
  // Everything below is written in prose above as well, and it has to be
548
741
  // written twice: the reader this matters most to is the one holding the
@@ -550,7 +743,7 @@ export const PublicCardSchema = z
550
743
  // money on what this card claims. `as_of` in particular means something
551
744
  // narrower here than the same name means elsewhere in this contract, and a
552
745
  // reader who assumed otherwise would trust a stale number.
553
- description: "A product an agent can buy, projected from the card its merchant published. as_of is when the price shown here was published, and nothing more: on a card whose price is checked at purchase it says nothing about how fresh that check will be — elsewhere in this contract the same name means the moment a live answer was true. price_checked_at_purchase says the merchant is asked for a price at the moment of purchase, not that they answer; what happens when they are silent depends on the mode and belongs to the gateway. The number above is what an agent compares when choosing and may not be what the sale goes through at. Two rules hold beyond the shape: a synchronous product names no delivery deadline, because it is delivered inside a response budget that is the same for every product on the platform, and only a product whose merchant is asked to confirm names a confirmation deadline. No field here names who is selling: this contract has no shape for a merchant's public identity.",
746
+ description: "A product an agent can buy, projected from the card its merchant published. as_of is when the price shown here was published, and nothing more: on a card whose price is checked at purchase it says nothing about how fresh that check will be — elsewhere in this contract the same name means the moment a live answer was true. price_checked_at_purchase says the merchant is asked for a price at the moment of purchase, not that they answer; what happens when they are silent depends on the mode and belongs to the gateway. The number above is what an agent compares when choosing and may not be what the sale goes through at. Two rules hold beyond the shape: a synchronous product names no delivery deadline, because it is delivered inside a response budget that is the same for every product on the platform, and only a product whose merchant is asked to confirm names a confirmation deadline. seller is who sells, as the merchant gave it: Agentify did not check the name or the site. fulfillment is a word whose known values are listed beside it, and more may be added: a reader keeps a default arm, and a card whose mode it does not know is one to pass over, not a reason to stop reading the catalog. This document and every part inside it may also gain fields; a reader ignores the ones it does not know. The type of a declared field in params and result is a closed list, and a card with a type a reader does not know is one to pass over too.",
554
747
  });
555
748
  /**
556
749
  * The card as an agent reads it, built from the card the merchant published.
@@ -562,12 +755,14 @@ export const PublicCardSchema = z
562
755
  * every agent by default, and the first anybody heard of it would be a
563
756
  * merchant's pricing address in a public catalog.
564
757
  *
565
- * The two things the card cannot know are passed in: the catalog identifier we
566
- * issued when it was published, and the moment its price was published.
758
+ * The three things the card cannot know are passed in: the catalog identifier
759
+ * we issued when it was published, the moment its price was published, and
760
+ * who sells it as their merchant stands now.
567
761
  */
568
762
  export const publicCardOf = (card, issued) => {
569
763
  const common = {
570
764
  id: issued.id,
765
+ seller: issued.seller,
571
766
  title: card.title,
572
767
  description: card.description,
573
768
  price: card.price,
@@ -688,8 +883,11 @@ export const MerchantCardSchema = z
688
883
  id: IdentifierSchema,
689
884
  /** When this version of the card was published. */
690
885
  as_of: TimestampSchema,
691
- /** The card exactly as its merchant published it. */
692
- card: CardSchema,
886
+ /**
887
+ * The card exactly as its merchant published it, held to the rules of a
888
+ * stored card rather than to the publish door's.
889
+ */
890
+ card: StoredCardSchema,
693
891
  /** What a purchase of this card meets right now. */
694
892
  selling: SellingStateSchema,
695
893
  /** Whether the pause is this card's own, rather than the whole catalog's. */