The shared /packs spec
One manifest. Many hosts.
Refusal instant and nameable.
packstruct defines how a /packs directory works - manifest, entry contract, lifecycle, refusals - so an author who writes one pack knows how packs work everywhere. Currently hosted by crashtest (scenario profile) and poststruct (check profile).
# the directory contract packs/ public/ <pack-name>/ pack.json # REQUIRED: schemaVersion 1, name matches dir, entry exists <entry> # REQUIRED: created by pack.json "entry" ok tg/data-boundary v1.0.0 - entry ships, name matches its dir ERR tg/broken-ref entry "index.js" does not exist - instant nameable refusal (crashtest#16)
What froze at v1
The whole shared surface - tight by design. Full normative text on the spec page.
section The contract
packs/public/<name>/ carries a frozen pack.json (schemaVersion 1, name matches directory, entry must exist) plus its entry module. A manifest declaring a file that doesn't ship is an instant, nameable refusal - never a deep import stack trace.
section The linter
npx packstruct lint packs/ - zero dependencies, kills the missing-entry class at author time, in any fleet repo. Machine-readable with --json.
section Profiles
The manifest surface never forks; execution semantics are declared as profiles: scenario (crashtest: hostile scenarios against a transport, isolated) and check (poststruct: pure-reading verifiers over files on disk).
section Lifecycle
Packs are explicit - a pack runs only because the host's config enumerates it. Pack ids are namespaced by pack slug; every pack ships self-audit fixtures; extra manifest keys are minimum-spec and left alone.
The pipeline
How a pack goes from directory to loaded, everywhere.
packs/public/, one pack.json, one entry module - any host, same three fields minimum.npx packstruct lint packs/ catches the manifest-points-at-nothing class before any host ever loads it.Why one spec
The contract already forked in practice - packstruct names the shared part so drift is declared, not accidental.
One address for authors
"How do I write a pack?" has one answer: read packstruct, pick your app's declared profile. No archaeology in two repos.
Frozen from reality
v1 is what crashtest's eight production packs already ship, verified pack-by-pack: nothing to migrate, poststruct adopts fresh.
Cheap future consumers
Any struct-family tool that wants a /packs dir adopts the spec instead of inventing a third dialect.
Conformance, not vibes
The lint CLI is the first artifact; a conformance harness (packstruct audit-conformance) follows at the 1.0.0 ladder rung.