5.8 KiB
@suaveplan/api-manifest — Zod manifest schema, validators, dep + capability resolvers
Package: @suaveplan/api-manifest
Status: PROPOSED
Type: new-package
Stage: 1 (Contracts) — see umbrella openspec/changes/api-plugin-foundation/
Why
Every plugin in the API foundation ships a plugin.json (design §3). The host validates that manifest at install AND at every boot — a manifest that drifted between install and reload is a hard failure. Without a single Zod-validated source of truth for the manifest shape, every consumer (host loader, admin API, CLI scaffolder, OpenAPI emitter, definePlugin typings) would re-derive the schema and the surface would drift instantly. api-manifest is that source of truth: runtime Zod schemas + parsed types + the two graph resolvers (dependsOn → topological install order; requiredCapabilities / optionalCapabilities → capability resolution against the registered adapters).
Per design §3: "Manifest is the source of truth for declarative facts... The host validates and acts on these before loading the plugin's code."
What changes
Net-new package with both runtime Zod schemas and graph algorithms. Subpath exports per concern.
- ADDED:
./schema— Zod schemas for every top-level manifest key (identity,compat,isolation,permissions,dependsOn,routes,graphql,websockets,sse,sagas,twoPhaseCommit,database,jobs,events,secrets,config,scopes,telemetry,openapi,errors,uninstall) plus the compositepluginManifestSchema. - ADDED:
./parse—parseManifest(input: unknown): PluginManifest(throwsMANIFEST.PARSE_FAILEDwith Zod issues attached);parseManifestSafe(input)returning a discriminated result. - ADDED:
./validators— cross-field validators that Zod alone can't express:validateSagaOwnership(every step plugin is listed independsOnor is self),validatePermissionsWhenSandboxed(sandboxed isolation requirespermissions.budgets),validateOpenApiRequiresGraphqlOrRoutes,validateErrorStatusMapShape(every code matchesPLUGIN.<UPPER>.<UPPER>and status is 4xx/5xx). - ADDED:
./dep-graph—resolveInstallOrder(manifests): readonly PluginManifest[](topological sort overdependsOn); detects cycles →MANIFEST.DEPENDENCY_CYCLE; detects missing deps →MANIFEST.MISSING_DEPENDENCY; detects version mismatch (semver) →MANIFEST.DEPENDENCY_VERSION_MISMATCH. - ADDED:
./capability-resolver—resolveCapabilities({ manifest, registered }): CapabilityResolution; reports missing required capabilities (MANIFEST.CAPABILITY_REQUIRED_MISSING), missing optional capabilities (warning bucket, no error), version mismatches (MANIFEST.CAPABILITY_VERSION_MISMATCH). - ADDED:
./hash—hashManifest(manifest): stringreturning"sha256:<hex>"over a canonical JSON form (sorted keys, stable separators) — used byRollbackJournalentries and the plugin ledger to detect drift across reload. - ADDED:
./error-codes— frozenMANIFEST_CODESregistry viadefineCodes. - ADDED:
./testing—runManifestConformance(parse)reusable harness for downstream consumers that want to verify parser equivalence.
Out of scope
- Loading
plugin.jsonfrom disk — that'sapi-loader. This package only parses anunknowninput. definePluginruntime — lives inapi-host.api-manifestonly exports the manifest side of the contract.- Manifest migrations (v1 → v2) — forward-compat only at v1. Migrators will land when v2 ships.
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
| Top-level manifest shape | §3 (full JSON example, lines 213-368) |
isolation enum + permissions budgets |
§2.2, §3 (lines 229-242) |
dependsOn resolver semantics |
§3, §4 (install order constraints) |
requiredCapabilities / optionalCapabilities |
§3, §5.2 |
| Sagas ownership validation | §3 ("sagas" block), §9.1 |
Errors statusMap shape |
§3 ("errors" block), §12.1 |
| Manifest hash for RollbackJournal | §8.1a (manifestHash field) |
Requirements with no source line
| Requirement | Justification |
|---|---|
runManifestConformance harness |
Pattern established by i18n-shared conformance harnesses; downstream tooling (admin CLI, IDE plugin) needs a way to assert parser equivalence. |
Verification approach
src/schema/*.test.ts— golden manifest fixtures (minimal, maximal, every edge case from §3);expectTypeOfon inferredPluginManifestmatching the type imported from@suaveplan/api-types.src/parse/parse.test.ts— accept/reject suites; verifies Zod issues are surfaced verbatim onPARSE_FAILED.src/validators/*.test.ts— every cross-field validator: accept path, every rejection branch with the right code.src/dep-graph/dep-graph.test.ts— topological-order property tests (Fisher-Yates shuffled input → identical sorted output); cycle detection; missing-dep detection.src/capability-resolver/capability-resolver.test.ts— required vs optional, version semver matching, missing capability surfacing.src/hash/hash.test.ts— key-order independence; nested-object stability; sha256 KAT vector.tests/dep-graph.test.ts— pins this package depends only on@suaveplan/api-types,@suaveplan/error,zod, and@suaveplan/types(logger contract for warnings).
Migration impact
None — additive only. Nothing in the monorepo depends on @suaveplan/api-manifest yet. The umbrella's Stage-2 (api-host) is the first consumer.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/api-manifest to enumerate. No dependents at Stage 1 — every consumer lands in Stage 2 or later.
- (none) — [x] no action needed (reason: net-new package; no existing consumers in the genesis branch)