Normative, frozen at schemaVersion 1
The spec
The long-form normative text is spec/SPEC.md in the repo (and mirrored at docs.creadev.org/packstruct). This page is the readable surface.
Directories
Packs live at packs/public/<pack-name>/, carrying a required pack.json plus the entry file it declares. That is the whole surface.
The manifest, v1
Drawn from what crashtest's eight production packs already ship - verified pack-by-pack, no fleet migration.
| Field | Type | Rules |
|---|---|---|
| schemaVersion | integer | MUST be 1; bumps are additive-first (v2 adds optional fields; removals are a major bump with a migration note) |
| name | string | kebab-case, MUST match its directory - mismatch is a load error, never a silent success |
| description | string | one line, human-readable |
| version | string | dotted semver-ish MAJOR.MINOR[.PATCH] |
| entry | string | relative path inside the pack, MUST exist - declaring a file that doesn't ship is an instant refusal, not a stack trace (crashtest#16) |
| armedByKey | string? | OPTIONAL. Present = the host MAY require an arming key before activation. Absent = the host's own lifecycle governs |
Host-owned keys the engine also recognizes (env, plugins, package) are acknowledged by name and never refused - the linter is strict against spec norms and only those norms.
Profiles (declared, not hidden)
- scenario - crashtest: default export
{ register(ctx) }, hostile scenarios against a transport, so it requires the host's isolation guarantees (fake transport), and may use the armed-key ceremony - check - poststruct: default export
{ checks: [Check, ...] }, declarative pure-reading verifiers over files on disk; no transport, no network by default
A pack declares nothing about profiles - the host declares which profile it loads. The scenario contract links to crashtest's reference rather than copying it (doc drift beats indirection).
Lifecycle rules (spec-fixed everywhere)
- Packs are explicit - a pack runs only because the host's config enumerates it; no implicit discovery
- Ids are namespaced by pack slug inside the host's id space; cross-namespace collision at load = refuse to start
- Local-path sources only in v1; remote/npm sources arrive as an explicit additive spec version, never silently
- Self-audit fixtures ship with every pack -
test/fixtures/proving the pack does what it claims; engines adopt this first, the spec formalizes after
App-owned (declared in the app's README, not the spec): arming ceremony details, execution sandbox, scheduling, output formats.
Versioning
Each app pins the spec version it implements ("implements: "packstruct@1") in its README. Engine changes land as code first; the spec formalizes after - never the reverse.