first commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# `validateErrorStatusMapShape`
|
||||
|
||||
## Purpose
|
||||
|
||||
Defence-in-depth re-check of the `errors.statusMap` shape. The Zod schema already rejects malformed entries; this validator collects every offender (rather than fail-fast on the first) so admin tooling can render the full diagnostic in one pass — operators get a complete fix list instead of fix-one-rerun-find-next.
|
||||
|
||||
## Features
|
||||
|
||||
- Walks every map entry — no early exit
|
||||
- Surfaces code-format violations separately from status-range violations
|
||||
- Empty / absent map is a no-op success
|
||||
- Reuses `errorCodeRegex` from the schema layer to avoid drift
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { validateErrorStatusMapShape, parseManifest } from "@suaveplan/api-manifest";
|
||||
validateErrorStatusMapShape(parseManifest(raw));
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { validateErrorStatusMapShape } from "@suaveplan/api-manifest/validators";
|
||||
import { parseManifest } from "@suaveplan/api-manifest/parse";
|
||||
|
||||
const manifest = parseManifest(raw);
|
||||
const check = validateErrorStatusMapShape(manifest);
|
||||
if (!check.ok) {
|
||||
for (const o of (check.error.context?.offenders as { code: string; status: number; reason: string }[])) {
|
||||
console.error(o.code, o.status, o.reason);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `validateErrorStatusMapShape(manifest: PluginManifest): ValidationResult`
|
||||
- Failure code: `MANIFEST.INVALID_ERROR_STATUS_MAP` with `context.offenders: { code, status, reason }[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Because the Zod schema rejects ill-shaped entries up front, the only way this validator ever finds an offender is via direct programmatic construction (skipping `parseManifest`) — but that path is real (the host builds plugin manifests in tests, the registry seeds them, etc.). The validator's batch-mode contract means it remains the canonical check at the install gate even when the schema has already run.
|
||||
|
||||
## Cross-package Notes
|
||||
|
||||
This module is part of the `@suaveplan/api-manifest` Stage-1 contract package. It sits beneath every Stage-2 consumer (`api-host`, `api-loader`, the admin CLI, the IDE plugin) and above the foundation packages (`@suaveplan/error`, `@suaveplan/types`). The strict-mode top-level rejection of unknown keys means upgrading from manifest v1 to v2 is an OpenSpec-driven event: a new key landing in a consumer manifest is a parse failure rather than a silent drift, which is exactly the contract this stage exists to guarantee.
|
||||
|
||||
When the host reloads a plugin without a full restart, every manifest field this module validates is compared against the install-time `manifestHash`. A mismatch triggers a hard rollback via the `RollbackJournal` because the host's invariants (route mounts, registered capabilities, declared scopes) are derived from these exact bytes. Treat the schemas and validators here as the contract surface, not just an input filter — downstream code reads `manifest.foo.bar` and relies on those keys being present, well-typed, and non-undefined in the documented optionality regions.
|
||||
@@ -0,0 +1,46 @@
|
||||
# `validateOpenApiRequiresGraphqlOrRoutes`
|
||||
|
||||
## Purpose
|
||||
|
||||
Rejects manifests that declare an OpenAPI contribution but expose neither HTTP routes nor a GraphQL contribution. The OpenAPI emitter needs a surface to document; without one the emission step would produce an empty document that wastes admin-UI real estate.
|
||||
|
||||
## Features
|
||||
|
||||
- No-op success when `openapi.contributes` is false or absent
|
||||
- Accepts either `routes` (any prefix) or `graphql.contributes === true`
|
||||
- Stable `MANIFEST.OPENAPI_REQUIRES_GRAPHQL_OR_ROUTES` code
|
||||
- Manifest name surfaced in the error context
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { validateOpenApiRequiresGraphqlOrRoutes, parseManifest } from "@suaveplan/api-manifest";
|
||||
const r = validateOpenApiRequiresGraphqlOrRoutes(parseManifest(raw));
|
||||
if (!r.ok) throw r.error;
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { validateOpenApiRequiresGraphqlOrRoutes } from "@suaveplan/api-manifest/validators";
|
||||
import { parseManifest } from "@suaveplan/api-manifest/parse";
|
||||
|
||||
const manifest = parseManifest(raw);
|
||||
const check = validateOpenApiRequiresGraphqlOrRoutes(manifest);
|
||||
if (!check.ok) console.warn("OpenAPI contrib will be empty:", manifest.name);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `validateOpenApiRequiresGraphqlOrRoutes(manifest: PluginManifest): ValidationResult`
|
||||
- Failure code: `MANIFEST.OPENAPI_REQUIRES_GRAPHQL_OR_ROUTES`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
GraphQL contribution is detected by `graphql.contributes === true` rather than mere presence of the block; a `{ contributes: false }` block is a legal opt-out state. Routes contribution is detected by mere presence — any prefix counts because the host will introspect handlers at boot. The check is intentionally cheap: a single boolean expression over four readily-available fields.
|
||||
|
||||
## Cross-package Notes
|
||||
|
||||
This module is part of the `@suaveplan/api-manifest` Stage-1 contract package. It sits beneath every Stage-2 consumer (`api-host`, `api-loader`, the admin CLI, the IDE plugin) and above the foundation packages (`@suaveplan/error`, `@suaveplan/types`). The strict-mode top-level rejection of unknown keys means upgrading from manifest v1 to v2 is an OpenSpec-driven event: a new key landing in a consumer manifest is a parse failure rather than a silent drift, which is exactly the contract this stage exists to guarantee.
|
||||
|
||||
When the host reloads a plugin without a full restart, every manifest field this module validates is compared against the install-time `manifestHash`. A mismatch triggers a hard rollback via the `RollbackJournal` because the host's invariants (route mounts, registered capabilities, declared scopes) are derived from these exact bytes. Treat the schemas and validators here as the contract surface, not just an input filter — downstream code reads `manifest.foo.bar` and relies on those keys being present, well-typed, and non-undefined in the documented optionality regions.
|
||||
@@ -0,0 +1,48 @@
|
||||
# `validatePermissionsWhenSandboxed`
|
||||
|
||||
## Purpose
|
||||
|
||||
Enforces that a `sandboxed`-tier plugin declares `permissions.budgets`. Without budgets the host can't enforce CPU/memory/wall-clock quotas, and the sandbox guarantees would be a stronger claim than the runtime actually delivers. This is the canonical case where Zod alone can't capture the invariant — the requirement spans two top-level keys.
|
||||
|
||||
## Features
|
||||
|
||||
- No-op success for non-sandboxed isolation tiers
|
||||
- Single-shot validation — fails iff isolation is sandboxed and budgets is missing
|
||||
- Stable `MANIFEST.SANDBOX_REQUIRES_BUDGETS` code
|
||||
- Manifest name surfaced in the error context for admin tooling
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { validatePermissionsWhenSandboxed, parseManifest } from "@suaveplan/api-manifest";
|
||||
const r = validatePermissionsWhenSandboxed(parseManifest(raw));
|
||||
if (!r.ok) throw r.error;
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { validatePermissionsWhenSandboxed, type ValidationResult } from "@suaveplan/api-manifest/validators";
|
||||
import { parseManifest } from "@suaveplan/api-manifest/parse";
|
||||
|
||||
function install(raw: unknown): void {
|
||||
const manifest = parseManifest(raw);
|
||||
const checks: ValidationResult[] = [validatePermissionsWhenSandboxed(manifest)];
|
||||
for (const c of checks) if (!c.ok) throw c.error;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `validatePermissionsWhenSandboxed(manifest: PluginManifest): ValidationResult`
|
||||
- Failure code: `MANIFEST.SANDBOX_REQUIRES_BUDGETS` with `context.name`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`permissions.budgets` is the host's hook for the quota enforcer — values can be partial (only `cpuPercent` set, no `memoryMb`) and the enforcer applies host defaults to missing fields. The validator only requires the block to be present, not for every sub-field to be populated; that lets a plugin opt into CPU caps without committing to a specific memory cap. The non-sandboxed-tier no-op return is important for the install gate's batch-validation path.
|
||||
|
||||
## Cross-package Notes
|
||||
|
||||
This module is part of the `@suaveplan/api-manifest` Stage-1 contract package. It sits beneath every Stage-2 consumer (`api-host`, `api-loader`, the admin CLI, the IDE plugin) and above the foundation packages (`@suaveplan/error`, `@suaveplan/types`). The strict-mode top-level rejection of unknown keys means upgrading from manifest v1 to v2 is an OpenSpec-driven event: a new key landing in a consumer manifest is a parse failure rather than a silent drift, which is exactly the contract this stage exists to guarantee.
|
||||
|
||||
When the host reloads a plugin without a full restart, every manifest field this module validates is compared against the install-time `manifestHash`. A mismatch triggers a hard rollback via the `RollbackJournal` because the host's invariants (route mounts, registered capabilities, declared scopes) are derived from these exact bytes. Treat the schemas and validators here as the contract surface, not just an input filter — downstream code reads `manifest.foo.bar` and relies on those keys being present, well-typed, and non-undefined in the documented optionality regions.
|
||||
@@ -0,0 +1,46 @@
|
||||
# `validateSagaOwnership`
|
||||
|
||||
## Purpose
|
||||
|
||||
Cross-field validator that rejects sagas whose step list references a plugin neither owned by this manifest nor declared as a dependency. Catches typos and missing `dependsOn` entries at parse time rather than at runtime saga dispatch, which is exactly the kind of drift that's expensive to debug in production.
|
||||
|
||||
## Features
|
||||
|
||||
- Returns `{ ok: false }` with the full list of offending step references
|
||||
- Treats the manifest's own `name` as a self-reference
|
||||
- Walks every owned saga's step list, not just the first
|
||||
- Empty `sagas.owns` is a no-op success
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { validateSagaOwnership, parseManifest } from "@suaveplan/api-manifest";
|
||||
const result = validateSagaOwnership(parseManifest(raw));
|
||||
if (!result.ok) throw result.error;
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import { validateSagaOwnership, parseManifest } from "@suaveplan/api-manifest";
|
||||
const manifest = parseManifest(raw);
|
||||
const result = validateSagaOwnership(manifest);
|
||||
if (!result.ok) {
|
||||
console.error("offenders", result.error.context?.offenders);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `validateSagaOwnership(manifest: PluginManifest): ValidationResult`
|
||||
- Failure code: `MANIFEST.INVALID_SAGA_OWNERSHIP` with `context.offenders: { saga, step, plugin }[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The validator collects every offender in one pass instead of fail-fast on the first; admin tooling needs the full list to render a single diagnostic dialog rather than make the operator fix-rerun-fix-rerun. `participatesIn` is intentionally not cross-checked here — that's a host-level concern because the owning plugin lives in a different manifest and the host has the full set. This validator is bounded by the size of the local manifest only.
|
||||
|
||||
## Cross-package Notes
|
||||
|
||||
This module is part of the `@suaveplan/api-manifest` Stage-1 contract package. It sits beneath every Stage-2 consumer (`api-host`, `api-loader`, the admin CLI, the IDE plugin) and above the foundation packages (`@suaveplan/error`, `@suaveplan/types`). The strict-mode top-level rejection of unknown keys means upgrading from manifest v1 to v2 is an OpenSpec-driven event: a new key landing in a consumer manifest is a parse failure rather than a silent drift, which is exactly the contract this stage exists to guarantee.
|
||||
|
||||
When the host reloads a plugin without a full restart, every manifest field this module validates is compared against the install-time `manifestHash`. A mismatch triggers a hard rollback via the `RollbackJournal` because the host's invariants (route mounts, registered capabilities, declared scopes) are derived from these exact bytes. Treat the schemas and validators here as the contract surface, not just an input filter — downstream code reads `manifest.foo.bar` and relies on those keys being present, well-typed, and non-undefined in the documented optionality regions.
|
||||
Reference in New Issue
Block a user