Files
2026-05-27 23:29:53 +01:00

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 composite pluginManifestSchema.
  • ADDED: ./parse — parseManifest(input: unknown): PluginManifest (throws MANIFEST.PARSE_FAILED with 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 in dependsOn or is self), validatePermissionsWhenSandboxed (sandboxed isolation requires permissions.budgets), validateOpenApiRequiresGraphqlOrRoutes, validateErrorStatusMapShape (every code matches PLUGIN.<UPPER>.<UPPER> and status is 4xx/5xx).
  • ADDED: ./dep-graph — resolveInstallOrder(manifests): readonly PluginManifest[] (topological sort over dependsOn); 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): string returning "sha256:<hex>" over a canonical JSON form (sorted keys, stable separators) — used by RollbackJournal entries and the plugin ledger to detect drift across reload.
  • ADDED: ./error-codes — frozen MANIFEST_CODES registry via defineCodes.
  • ADDED: ./testing — runManifestConformance(parse) reusable harness for downstream consumers that want to verify parser equivalence.

Out of scope

  • Loading plugin.json from disk — that's api-loader. This package only parses an unknown input.
  • definePlugin runtime — lives in api-host. api-manifest only 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); expectTypeOf on inferred PluginManifest matching the type imported from @suaveplan/api-types.
  • src/parse/parse.test.ts — accept/reject suites; verifies Zod issues are surfaced verbatim on PARSE_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)