Skip to content

fix(cards): one catalog, one host, one primary key - #129

Merged
Wolfe-Jam merged 3 commits into
mainfrom
fix/catalog-host
Sep 16, 2026
Merged

Wolfe-Jam merged 3 commits into
mainfrom
fix/catalog-host

Conversation

@Wolfe-Jam

@Wolfe-Jam Wolfe-Jam commented Sep 16, 2026

Copy link
Copy Markdown
Owner

The CLI's faf cards --target catalog and the pack API's projectAiCatalog projected different catalogs from the same .fafa. Pre-launch, with nothing published yet, this fixes the projection rather than preserving it.

1. The catalog names its host

faf cards wrote {specVersion, entries} — AI Catalog Level 1 "minimal". The pack API named the host and reached Level 2 "discoverable". Both paths now derive the host once.

displayName is the field that does the lifting. The validator takes an empty one as invalid, not as minimal, so a .fafa that names nobody gets no host at all — minimal and valid beats discoverable and broken. That also closes a latent hole on the pack path, which would have emitted displayName: "" for a vendorless .fafa.

Spec, Host Info: displayName MUST; identifier is "a DID or domain name" — so the bare host the CLI emits is the right form.

2. One primary key

With the host named, the file contradicted itself. Same .fafa, before:

host rows
CLI weather.acme.example urn:air:acme.example:a2a:Weather Bot
pack API weather.acme.example urn:air:weather.acme.example:a2a:weather-bot

The CLI keyed rows off the homepage host and the raw agent.name; the pack projector keys them off the domain the .fafa declares and the handle. The raw name is the worse half — a space inside a URN. The validator passes it (it doesn't parse URN syntax), so it would have shipped broken-but-valid.

Spec, Catalog Entry: urn:air:{publisher}:{namespace}:{name} — publisher is "the domain name of the organization publishing the artifact", name is "the stable, unique name". An identifier is a catalog's primary key: cheap to change now, breaking for every consumer later.

A .fafa that names no domain is now refused in one line (exit 2) rather than published as urn:air:local:… — the same stance as "will not invent a door". The A2A card's passport URL comes off the same domain, so a card and a catalog never disagree about where the .fafa lives.

Unchanged for any .fafa whose homepage host is its declared domain and whose name is already a handle — faf.one's own included.

Receipts

ai-catalog-cli v0.2.2 (sha256-checked release binary), against files the CLI actually wrote:

Case Result
Fresh catalog discoverable, valid
Existing catalog with no host discoverable; a foreign row kept byte for byte
Catalog with another host untouched, still theirs
Second run byte-identical, reported unchanged
Divergent .fafa (declared domain ≠ homepage, name with a space) host and every row now agree; no space in any URN
.fafa with no domain refused, exit 2, nothing written

The added host follows the file's own layout — proven on a hand-written four-space catalog.

Tests

2047/2047 green · lint 0 errors. The load-bearing new test asserts the two projectors produce identical identifiers and URLs from one .fafa.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KnHgQTeSEV6uATCGumM1nz

`faf cards --target catalog` wrote {specVersion, entries} — AI Catalog
Level 1 "minimal". The pack API's projectAiCatalog, from the same .fafa,
named the host and reached Level 2 "discoverable". One projector, two
levels: the CLI's own catalog validated a level below its API's.

Both paths now derive the host once, in pack.ts. `displayName` is the
field that does the lifting — the validator takes an empty one as
*invalid*, not as minimal — so a .fafa that names nobody gets no host at
all: minimal and valid beats discoverable and broken. `identifier` rides
along when the .fafa says where it lives.

On a catalog faf shares, `host` is the one key it may add, and only to a
catalog that names none: a host already there is the site's own and is
left byte for byte, in the file's own layout.

Receipts (ai-catalog-cli v0.2.2, sha256-checked release binary):
  fresh catalog          → discoverable, valid
  existing minimal one   → minimal → discoverable, foreign row untouched
  foreign host present   → untouched, still theirs
  second run             → byte-identical

4 of the 7 new tests fail without this change; the other 3 guard what it
must not do. 2045/2045 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnHgQTeSEV6uATCGumM1nz
@vercel

vercel Bot commented Sep 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
faf-cli Ready Ready Preview Sep 16, 2026 2:45pm UTC

Pre-launch, so the right identifiers matter more than the old ones.

The CLI keyed its rows off the homepage host and the raw `agent.name`;
the pack projector keys them off the domain the .fafa *declares*
(agent.id's urn:air, metadata.cards.domain, else the homepage) and the
handle. Same .fafa, two primary keys — and once the host was named, the
file contradicted itself: host `weather.acme.example`, rows
`urn:air:acme.example:…`.

The raw name was the worse half. `agent.name: Weather Bot` published
`urn:air:acme.example:a2a:Weather Bot` — a space inside a URN. The
validator passes it (it does not parse URN syntax), so it would have
shipped broken-but-valid, and an identifier is a catalog's primary key:
cheap to change now, breaking for every consumer later.

AI Catalog spec, Catalog Entry: `urn:air:{publisher}:{namespace}:{name}`
— publisher is "the domain name of the organization publishing the
artifact", name is "the stable, unique name". Host Info: identifier is
"a DID or domain name", so the bare host the CLI emits is the right
form.

A .fafa that names no domain is now refused in one line (exit 2) rather
than published as `urn:air:local:…` — the same stance as "will not
invent a door". The A2A card's passport URL comes off the same domain,
so a card and a catalog never disagree about where the .fafa lives.

Unchanged for any .fafa whose homepage host is its declared domain and
whose name is already a handle — faf.one's own included.

2047/2047 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnHgQTeSEV6uATCGumM1nz
@Wolfe-Jam Wolfe-Jam changed the title fix(cards): name the catalog's host, so faf cards reaches discoverable fix(cards): one catalog, one host, one primary key Sep 16, 2026
`faf cards --target ard` writes `.well-known/ard.json`: the catalog rows,
carrying the search hints ARD reads from the .fafa
(`metadata.cards.keywords` -> tags, `metadata.cards.examples` ->
representativeQueries).

On the path: the spec prose and its own tool disagree, and the tool is
newer. ARD v0.9 §4.1/§6.1 name /.well-known/ai-catalog.json; the
conformance CLI v0.9.1 says "A consumer MUST fetch /.well-known/ard.json.
Consulting the predecessor /.well-known/ai-catalog.json is permitted
(MAY) but not required." Build ahead: write ard.json, and `--target
catalog,ard` writes both, as faf.one already serves both.

The document keeps specVersion + host. ARD ignores them ("top-level
members ignored by ARD (transport-defined)") but ai-catalog requires
specVersion and reads host, so one shape serves both specs — which is
what ARD §4 means by building on ai-catalog.

A manifest with no representativeQueries is valid and unfindable:
registries build their semantic index from that term. faf now says so in
one line and points at the .fafa key, rather than writing a card nobody
can find. The spec asks for 2-5.

Receipts (ard-spec conformance CLI v0.9.1, `manifest`):
  hints in the .fafa  -> PASS, 0 errors, 0 warnings
  no hints            -> PASS, 2 warnings, and faf printed the note first

And the receipt that settles the identifier change one commit back: the
identifier the CLI used to emit, `urn:air:acme.example:a2a:Weather Bot`,
fails ARD outright — three errors, "does not match RFC 8141 URN pattern".
The ai-catalog validator passed it. ARD does not.

2050/2050 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnHgQTeSEV6uATCGumM1nz
@Wolfe-Jam
Wolfe-Jam merged commit d399a6d into main Sep 16, 2026
11 checks passed
@Wolfe-Jam
Wolfe-Jam deleted the fix/catalog-host branch September 16, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant