Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,3 @@ jobs:

- name: Typecheck
run: npm run typecheck

- name: OpenAPI schema pre-flight (strict — fails on unacknowledged drift)
run: npm run preflight
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ docs/reference/api/

# Claude Code hook logs
.claude/hooks/*.log

# Symlinks to external OpenAPI specs
schemas
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ npm run docs:serve # build typedoc, then run docs-site (Docusaurus) dev serv
npm run docs:build # build typedoc + docs-site static site
npm run docs:check # verify every public symbol has example coverage (CI gate)
npm run docs:check:warn # same, exit 0 even on gaps
npm run preflight # diff Zod schemas vs OpenAPI specs (strict; CI gate)
npm run preflight # diff Zod schemas vs OpenAPI specs in schemas/ (strict; local/pre-release)
npm run preflight:warn # same, exit 0 even on drift
```

`preflight` diffs the Zod schemas in `src/models/` against the OpenAPI specs in `specs/` and gates CI; run it before tagging a release or after API-side changes. Runnable usage examples live in `docs-site/examples/` (one `*.ts` per workflow) and are surfaced through the docs site, not via `npm run` scripts.
`preflight` diffs the Zod schemas in `src/models/` against the OpenAPI specs referenced by `schemas/`. `schemas/` is a **gitignored** directory of symlinks to a local pan.dev `openapi-specs` checkout (see `.gitignore`), so preflight is a **local / pre-release check, not a CI gate** — run it before tagging a release or after API-side changes. The exact spec files it reads are the explicit `MODELED_SPECS` list in `scripts/preflight-schemas.ts`; keep that list in sync with `src/models/`. Runnable usage examples live in `docs-site/examples/` (one `*.ts` per workflow) and are surfaced through the docs site, not via `npm run` scripts.

Run a single test file:

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ npm run docs:serve # build typedoc, then run docs-site (Docusaurus) dev serv
npm run docs:build # build typedoc + docs-site static site
npm run docs:check # verify every public symbol has example coverage (CI gate)
npm run docs:check:warn # same, exit 0 even on gaps
npm run preflight # diff Zod schemas vs OpenAPI specs (strict; CI gate)
npm run preflight # diff Zod schemas vs OpenAPI specs in schemas/ (strict; local/pre-release)
npm run preflight:warn # same, exit 0 even on drift
```

`preflight` diffs the Zod schemas in `src/models/` against the OpenAPI specs in `specs/` and gates CI; run it before tagging a release or after API-side changes. Runnable usage examples live in `docs-site/examples/` (one `*.ts` per workflow) and are surfaced through the docs site, not via `npm run` scripts.
`preflight` diffs the Zod schemas in `src/models/` against the OpenAPI specs referenced by `schemas/`. `schemas/` is a **gitignored** directory of symlinks to a local pan.dev `openapi-specs` checkout (see `.gitignore`), so preflight is a **local / pre-release check, not a CI gate** — run it before tagging a release or after API-side changes. The exact spec files it reads are the explicit `MODELED_SPECS` list in `scripts/preflight-schemas.ts`; keep that list in sync with `src/models/`. Runnable usage examples live in `docs-site/examples/` (one `*.ts` per workflow) and are surfaced through the docs site, not via `npm run` scripts.

Run a single test file:

Expand Down
61 changes: 37 additions & 24 deletions scripts/preflight-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* tsx scripts/preflight-schemas.ts --warn-only # report drift, exit 0
*/
import { readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative } from 'node:path';
import { ZodType } from 'zod';
Expand All @@ -23,7 +24,7 @@ import { resolveOpenApiName } from './preflight/resolve-name.js';
import { isAllowlisted } from './preflight/allowlist.js';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const SPECS_DIR = join(ROOT, 'specs');
const SCHEMAS_DIR = join(ROOT, 'schemas');
const MODELS_DIR = join(ROOT, 'src', 'models');

interface ModelSchema {
Expand All @@ -34,34 +35,46 @@ interface ModelSchema {
}

/**
* DLP specs the SDK actually models. Other yaml files in `specs/dlp/` describe DLP endpoints
* we have not implemented yet — loading them creates name collisions (e.g. their `Policy`
* component differs from the mgmt `Policy`) and false drift against unrelated Zod schemas.
* The exact OpenAPI spec files the SDK models, as paths under the gitignored `schemas/` alias
* (symlinks to a local pan.dev `openapi-specs` checkout). Listed explicitly rather than globbed:
* the alias tree also holds `-latest` duplicates and unmodeled specs (e.g. most of `dlp/`, whose
* `Policy` component collides with the mgmt `Policy`) that would produce false drift.
*
* Keep this in sync with the Zod models in `src/models/`. This runs locally / pre-release only —
* `schemas/` is gitignored, so preflight is not a CI gate (see CLAUDE.md).
*/
const DLP_SPEC_WHITELIST = new Set([
'DataFilteringProfiles.yaml',
'DataPatterns.yaml',
'DataProfiles.yaml',
'Dictionaries.yaml',
]);

async function findSpecFiles(dir: string, parentName?: string): Promise<string[]> {
const out: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await findSpecFiles(full, entry.name)));
} else if (entry.isFile() && (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))) {
if (parentName === 'dlp' && !DLP_SPEC_WHITELIST.has(entry.name)) continue;
out.push(full);
}
const MODELED_SPECS = [
'prisma-airs/scan/scan-service_latest.yaml',
'prisma-airs/management/mgmt-service_latest.yaml',
'prisma-airs-model-security/dataplane/AIRS-Model-Security-DataPlane-latest.yaml',
'prisma-airs-model-security/management/AIRS-Model-Security-Management.yaml',
'prisma-airs-redteam/data-plane/dp-openapi.yaml',
'prisma-airs-redteam/management/mp-openapi.yaml',
'prisma-airs-redteam/network-broker/AIRS-Red-Teaming-Network-Broker.yaml',
'dlp/DataFilteringProfiles.yaml',
'dlp/DataPatterns.yaml',
'dlp/DataProfiles.yaml',
'dlp/Dictionaries.yaml',
];

function findSpecFiles(): string[] {
if (!existsSync(SCHEMAS_DIR)) {
throw new Error(
`The 'schemas/' alias is missing. Preflight reads the gitignored 'schemas/' directory ` +
`(symlinks to a local pan.dev openapi-specs checkout). Set it up before running preflight — see CLAUDE.md.`,
);
}
return out;
return MODELED_SPECS.map((rel) => {
const full = join(SCHEMAS_DIR, rel);
if (!existsSync(full)) {
throw new Error(`Missing modeled schema file under schemas/: ${rel}`);
}
return full;
});
}

async function loadAllSpecs(): Promise<Map<string, OpenAPIV3.SchemaObject>> {
const files = await findSpecFiles(SPECS_DIR);
const files = findSpecFiles();
const merged = new Map<string, OpenAPIV3.SchemaObject>();
const collisions: string[] = [];
for (const path of files) {
Expand Down
10 changes: 10 additions & 0 deletions scripts/preflight/allowlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,16 @@ export const PREFLIGHT_ALLOWLIST: AllowlistEntry[] = [
reason:
'API returns `features` (per-adapter capability flags) on channels; not in upstream OpenAPI Channel.',
},

// ── CustomerAppObject ────────────────────────────────────────────────────────
{
schema: 'CustomerAppObject',
pathSubstring: 'tsg_id',
kind: 'extra-required-field',
reason:
'The latest mgmt spec marks `tsg_id` optional, but every customer app belongs to a TSG ' +
'(the SDK even builds the list URL from it), so Zod keeps it required for consumers.',
},
];

/**
Expand Down
Loading
Loading