Part of #73.
Problem
The artifact contract has grown two shapes and a misleading name:
- Packaged sites publish
dist.tar.gz containing dist/ + a root marketplace.json (one app). dist.tar.gz is indistinguishable from a repo's ordinary release package, and "marketplace" is the project's old name.
- Single-page bundles publish
dist.tar.gz containing bundle.json + one directory per doc (N apps).
scripts/fetch-apps.js, scripts/build-vite.js and src/utils/single-page.js therefore carry two manifest readers and two install paths that do almost the same thing. Every consumer (fetch, prebuilt, localPath) special-cases both.
Proposal: one artifact, one manifest
Every docs repo publishes exactly one release asset, kb-docs.tar.gz, whose root holds kb-docs.json plus one directory per app:
kb-docs.tar.gz
├─ kb-docs.json
├─ <slug-a>/
│ ├─ index.html ← entryPoint
│ └─ assets/…
└─ <slug-b>/ ← only bundles with several apps have more than one
└─ …
A packaged site is a manifest with one app whose directory is its built dist/. A single-page bundle is a manifest with N apps. The knowledge base no longer needs to know which one it is looking at.
kb-docs.json
kbVersion is a string so it can carry "1" today and "1.1" / "2" later without a type change. The knowledge base fails the build with a clear message on a version it does not understand. Unknown additional fields are ignored, so a newer publisher never breaks an older knowledge base on additive changes.
slug must be unique across the whole knowledge base; the build already enforces this (assertUniqueSlugs) and the error names both claimants.
- Apps in one bundle may carry a
kind field later ("site" | "single-page") if rendering needs to diverge; today the presence of pages or of more than one HTML file is enough, so it is not part of v1.
Manifest is the source of truth
Name, description, icon, tags, slug, entry point and pages live only in the docs repo (see #75 for the apps.json side). The contract must say so explicitly: the knowledge base never overrides them.
Rules that stay
Everything in contract/HEADLESS_RULES.md about the HTML itself stays valid (headless attribute, relative paths, no fixed header, design tokens), only re-homed under the new names from #77.
Scope
Out of scope
Build-side code changes (#75), the publishing actions (#76), and the identifier rename (#77) each have their own issue; this one owns the contract documents and schema so the others have something to implement against.
Hard cut
There is no compatibility window: no deployment consumes the old artifact yet, and every docs repo republishes through the action from #76. dist.tar.gz and marketplace.json are simply not recognised after this lands — the fetch error message should name kb-docs.tar.gz and link the contract.
Part of #73.
Problem
The artifact contract has grown two shapes and a misleading name:
dist.tar.gzcontainingdist/+ a rootmarketplace.json(one app).dist.tar.gzis indistinguishable from a repo's ordinary release package, and "marketplace" is the project's old name.dist.tar.gzcontainingbundle.json+ one directory per doc (N apps).scripts/fetch-apps.js,scripts/build-vite.jsandsrc/utils/single-page.jstherefore carry two manifest readers and two install paths that do almost the same thing. Every consumer (fetch,prebuilt,localPath) special-cases both.Proposal: one artifact, one manifest
Every docs repo publishes exactly one release asset,
kb-docs.tar.gz, whose root holdskb-docs.jsonplus one directory per app:A packaged site is a manifest with one app whose directory is its built
dist/. A single-page bundle is a manifest with N apps. The knowledge base no longer needs to know which one it is looking at.kb-docs.json{ "kbVersion": "1", // contract version; the only field the KB reads before validating "apps": [ { "slug": "user-guide", // ^[a-z0-9]+(-[a-z0-9]+)*$, 2–32 chars, globally unique → /knowledge-base/{slug}/ "name": "Knowledge Base User Guide", "description": "…", // 10–280 chars "icon": "book-open", // from the fixed icon set "tags": ["guide"], // ≤ 5 "entryPoint": "index.html", // relative to <slug>/, default index.html "pages": [ // optional navigation manifest; absent → crawl { "title": "Overview", "path": "index.html", "order": 1 }, { "title": "Publishing", "path": "docs/publishing/index.html", "order": 2, "section": "Guide" } ] } ] }kbVersionis a string so it can carry"1"today and"1.1"/"2"later without a type change. The knowledge base fails the build with a clear message on a version it does not understand. Unknown additional fields are ignored, so a newer publisher never breaks an older knowledge base on additive changes.slugmust be unique across the whole knowledge base; the build already enforces this (assertUniqueSlugs) and the error names both claimants.kindfield later ("site" | "single-page") if rendering needs to diverge; today the presence ofpagesor of more than one HTML file is enough, so it is not part of v1.Manifest is the source of truth
Name, description, icon, tags, slug, entry point and pages live only in the docs repo (see #75 for the
apps.jsonside). The contract must say so explicitly: the knowledge base never overrides them.Rules that stay
Everything in
contract/HEADLESS_RULES.mdabout the HTML itself stays valid (headless attribute, relative paths, no fixed header, design tokens), only re-homed under the new names from #77.Scope
contract/schema.json→contract/kb-docs.schema.json: JSON Schema (draft-07) forkb-docs.jsonas above, with$idpointing at the raw GitHub URL so actions and the KB build validate against the same documentcontract/HEADLESS_RULES.mdandcontract/SINGLE_PAGE.md: replacemarketplace.json/bundle.json/dist.tar.gzsections with the unified layout; add a shortcontract/ARTIFACT.mdthat is the single normative description of the tarball (layout, manifest, versioning, size guidance) and link to it from bothscripts/artifacts.jsenforces them today: no absolute members, no.., no symlinks; add "no members outsidekb-docs.jsonand<slug>/…" so stray files cannot become routestests/fixtures/docs-example.dist.tar.gzandtests/fixtures/single-page-bundle/regenerated in the new layout;scripts/setup-test-apps.mjsand thebuild-integrityspec updatedREADME.md,AGENTS.md,CLAUDE.mdupdated to the new namesOut of scope
Build-side code changes (#75), the publishing actions (#76), and the identifier rename (#77) each have their own issue; this one owns the contract documents and schema so the others have something to implement against.
Hard cut
There is no compatibility window: no deployment consumes the old artifact yet, and every docs repo republishes through the action from #76.
dist.tar.gzandmarketplace.jsonare simply not recognised after this lands — the fetch error message should namekb-docs.tar.gzand link the contract.