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
24 changes: 21 additions & 3 deletions scripts/generate-zod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,21 @@ async function main() {
console.log('Reading spec from', SPEC_PATH);
const spec = JSON.parse(readFileSync(SPEC_PATH, 'utf8'));

const { flattened, inlinedDiscriminators, inlinedNullableDeductions } =
preprocessSpec(spec);
// Snapshot the unmodified spec for `generateSpecFacts` BEFORE
// preprocessing mutates it. The Postel's-Law relaxer drops multi-value
// enums on response-shape DTOs (e.g. `AlertChannelDto.channelType`),
// and a few facts are anchored on response DTOs as fallbacks. Reading
// facts off the original spec keeps the canonical value lists complete
// even after the relaxer runs, while the generated Zod schemas (which
// use `spec`) stay tolerant on the wire.
const originalSpec = JSON.parse(JSON.stringify(spec));

const {
flattened,
inlinedDiscriminators,
inlinedNullableDeductions,
relaxedEnums,
} = preprocessSpec(spec);
console.log(`Preprocessed spec (${Object.keys(spec.components?.schemas ?? {}).length} schemas)`);
if (flattened.length > 0) {
console.log(` Flattened circular oneOf: ${flattened.join(', ')}`);
Expand All @@ -143,8 +156,13 @@ async function main() {
` Inlined nullable deduction refs for: ${inlinedNullableDeductions.join(', ')}`,
);
}
if (relaxedEnums && relaxedEnums.length > 0) {
console.log(
` Relaxed response-DTO enums (Postel's Law): ${relaxedEnums.length} fields`,
);
}

generateSpecFacts(spec);
generateSpecFacts(originalSpec);

mkdirSync(dirname(OUTPUT_PATH), { recursive: true });

Expand Down
81 changes: 80 additions & 1 deletion scripts/lib/preprocess.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,75 @@ export function inlineNullableDeductionRefs(spec) {
return Array.from(rewritten);
}

/**
* Drop `enum` constraints from response-shape DTO properties so the
* generated Zod schemas decode unknown future enum values as plain
* strings (Postel's Law contract — see
* `mini/runbooks/api-contract.md` § 2.2 + § 3).
*
* Selection rules — must match the request-vs-response naming convention
* shared with mini's `relaxResponseEnums` post-processor:
*
* - Walks `components.schemas`. A schema is "response-shape" if its
* name matches `*Dto`, `*Response`, `SingleValueResponse*`,
* `TableValueResult*`, or `CursorPage*`.
* - A schema is "request-shape" (left alone) if its name matches
* `*Request` / `*Params` or starts lower-case (helpers).
* - Inside a response-shape schema, every property whose schema
* carries a multi-value `enum` (length ≥ 2) gets the `enum` key
* dropped so codegens emit `z.string()` / `str` / `string`.
* - SINGLE-VALUE enums are PRESERVED — those are the discriminator
* tags installed by `inlineDiscriminatorSubtypesWithInfo`.
* - Array-typed properties get the same treatment on `items.enum`.
*
* Idempotent: re-running on a relaxed spec is a no-op. Returns the list
* of `Schema.field` paths that were relaxed.
*/
export function relaxResponseEnumsInSpec(spec) {
const schemas = getSchemas(spec);
const relaxed = [];

function isResponseShape(name) {
if (/^[a-z]/.test(name)) return false;
if (/(Request|Params)$/.test(name)) return false;
return (
/(Dto|Response)$/.test(name) ||
/^(SingleValueResponse|TableValueResult|CursorPage)/.test(name)
);
}

function relaxProps(schemaName, properties) {
if (!properties) return;
for (const [propName, raw] of Object.entries(properties)) {
if (!isSchemaObj(raw)) continue;
if (Array.isArray(raw.enum) && raw.enum.length >= 2) {
delete raw.enum;
relaxed.push(`${schemaName}.${propName}`);
}
if (raw.items && isSchemaObj(raw.items)) {
if (Array.isArray(raw.items.enum) && raw.items.enum.length >= 2) {
delete raw.items.enum;
relaxed.push(`${schemaName}.${propName}[]`);
}
}
}
}

for (const [schemaName, schema] of Object.entries(schemas)) {
if (!isResponseShape(schemaName)) continue;
relaxProps(schemaName, schema.properties);
if (Array.isArray(schema.allOf)) {
for (const member of schema.allOf) {
if (isSchemaObj(member)) {
relaxProps(schemaName, member.properties);
}
}
}
}

return relaxed;
}

export function preprocessSpec(spec) {
setRequiredFields(spec);
setRequiredOnAllOfMembers(spec);
Expand All @@ -326,7 +395,17 @@ export function preprocessSpec(spec) {
// discriminator-based parents as abstract/empty ones.
const inlinedNullableDeductions = inlineNullableDeductionRefs(spec);
const flattened = flattenCircularOneOf(spec);
return { flattened, inlinedDiscriminators, inlinedNullableDeductions };
// Postel's Law: drop multi-value enums on response-shape DTOs so all
// codegens (Zod, Pydantic, Go) emit tolerant readers. MUST run AFTER
// discriminator inlining so we don't accidentally relax single-value
// discriminator tags (those are length-1 enums and skipped by design).
const relaxedEnums = relaxResponseEnumsInSpec(spec);
return {
flattened,
inlinedDiscriminators,
inlinedNullableDeductions,
relaxedEnums,
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
| `groupId` | string (uuid) | | ✓ | |
| `name` | string | ✓ | | |
| `description` | string | | ✓ | |
| `type` | "MONITOR" \| "GROUP" \| "STATIC" | ✓ | | |
| `type` | string | ✓ | | |
| `monitorId` | string (uuid) | | ✓ | |
| `resourceGroupId` | string (uuid) | | ✓ | |
| `currentStatus` | "OPERATIONAL" \| "DEGRADED_PERFORMANCE" \| "PARTIAL_OUTAGE" \| "MAJOR_OUTAGE" \| "UNDER_MAINTENANCE" | ✓ | | |
| `currentStatus` | string | ✓ | | |
| `showUptime` | boolean | ✓ | | |
| `displayOrder` | integer (int32) | ✓ | | |
| `pageOrder` | integer (int32) | ✓ | | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
| `id` | string (uuid) | ✓ | | |
| `statusPageId` | string (uuid) | ✓ | | |
| `title` | string | ✓ | | |
| `status` | "INVESTIGATING" \| "IDENTIFIED" \| "MONITORING" \| "RESOLVED" | ✓ | | |
| `impact` | "NONE" \| "MINOR" \| "MAJOR" \| "CRITICAL" | ✓ | | |
| `status` | string | ✓ | | |
| `impact` | string | ✓ | | |
| `scheduled` | boolean | ✓ | | |
| `scheduledFor` | string (date-time) | | ✓ | |
| `scheduledUntil` | string (date-time) | | ✓ | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
| `slug` | string | ✓ | | |
| `description` | string | | ✓ | |
| `branding` | StatusPageBranding | ✓ | | |
| `visibility` | "PUBLIC" \| "PASSWORD" \| "IP_RESTRICTED" | ✓ | | |
| `visibility` | string | ✓ | | |
| `enabled` | boolean | ✓ | | |
| `incidentMode` | "MANUAL" \| "REVIEW" \| "AUTOMATIC" | ✓ | | |
| `incidentMode` | string | ✓ | | |
| `componentCount` | integer (int32) | | ✓ | |
| `subscriberCount` | integer (int64) | | ✓ | |
| `overallStatus` | "OPERATIONAL" \| "DEGRADED_PERFORMANCE" \| "PARTIAL_OUTAGE" \| "MAJOR_OUTAGE" \| "UNDER_MAINTENANCE" | | ✓ | |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | | ✓ | Source that created/owns this status page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on pages created before this attribution column existed. |
| `overallStatus` | string | | ✓ | |
| `managedBy` | string | | ✓ | Source that created/owns this status page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on pages created before this attribution column existed. |
| `createdAt` | string (date-time) | ✓ | | |
| `updatedAt` | string (date-time) | ✓ | | |

Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
|---|---|---|---|---|
| `id` | string (uuid) | ✓ | | Unique alert channel identifier |
| `name` | string | ✓ | | Human-readable channel name |
| `channelType` | "email" \| "webhook" \| "slack" \| "pagerduty" \| "opsgenie" \| "teams" \| "discord" | ✓ | | Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL) |
| `channelType` | string | ✓ | | Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL) |
| `displayConfig` | any | | ✓ | |
| `createdAt` | string (date-time) | ✓ | | Timestamp when the channel was created |
| `updatedAt` | string (date-time) | ✓ | | Timestamp when the channel was last updated |
| `configHash` | string | | ✓ | SHA-256 hash of the channel config; use for change detection |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | | ✓ | Source that created/owns this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on channels created before this attribution column existed. |
| `managedBy` | string | | ✓ | Source that created/owns this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on channels created before this attribution column existed. |
| `lastDeliveryAt` | string (date-time) | | ✓ | Timestamp of the most recent delivery attempt |
| `lastDeliveryStatus` | string | | ✓ | Outcome of the most recent delivery (SUCCESS, FAILED, etc.) |

Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
| `name` | string | ✓ | | Human-readable name for this monitor |
| `type` | "HTTP" \| "DNS" \| "MCP_SERVER" \| "TCP" \| "ICMP" \| "HEARTBEAT" | ✓ | | Monitor protocol type |
| `config` | any | ✓ | | |
| `frequencySeconds` | integer (int32) | | ✓ | Check frequency in seconds (30–86400); null defaults to plan minimum (60s on most paid plans) |
| `frequencySeconds` | integer (int32) | | ✓ | Check frequency in seconds (10–86400); null defaults to plan minimum (60s on most paid plans) |
| `enabled` | boolean | | ✓ | Whether the monitor is active (default: true) |
| `regions` | string[] | | ✓ | Probe regions to run checks from, e.g. us-east, eu-west |
| `regions` | string[] | | ✓ | Probe regions to run checks from. Allowed values are deployment-dependent; production: us-east, us-west, eu-west, ap-south. |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | | ✓ | Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted; set to your surface so audit logs, drift detection, and analytics attribute correctly. |
| `environmentId` | string (uuid) | | ✓ | Environment to associate with this monitor |
| `assertions` | CreateAssertionRequest[] | | ✓ | Assertions to evaluate against each check result |
Expand All @@ -27,9 +27,9 @@
|---|---|---|---|---|
| `name` | string | | ✓ | New monitor name; null preserves current |
| `config` | any | | ✓ | |
| `frequencySeconds` | integer (int32) | | ✓ | New check frequency in seconds (30–86400); null preserves current |
| `frequencySeconds` | integer (int32) | | ✓ | New check frequency in seconds (10–86400); null preserves current |
| `enabled` | boolean | | ✓ | Enable or disable the monitor; null preserves current |
| `regions` | string[] | | ✓ | New probe regions; null preserves current |
| `regions` | string[] | | ✓ | New probe regions; null preserves current. Allowed values are deployment-dependent. |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | | ✓ | New ownership source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value |
| `environmentId` | string (uuid) | | ✓ | New environment ID; null preserves current (use clearEnvironmentId to unset) |
| `clearEnvironmentId` | boolean | | ✓ | Set to true to remove the environment association |
Expand All @@ -47,12 +47,12 @@
| `id` | string (uuid) | ✓ | | Unique monitor identifier |
| `organizationId` | integer (int32) | ✓ | | Organization this monitor belongs to |
| `name` | string | ✓ | | Human-readable name for this monitor |
| `type` | "HTTP" \| "DNS" \| "MCP_SERVER" \| "TCP" \| "ICMP" \| "HEARTBEAT" | ✓ | | |
| `type` | string | ✓ | | |
| `config` | any | ✓ | | |
| `frequencySeconds` | integer (int32) | ✓ | | Check frequency in seconds (30–86400) |
| `enabled` | boolean | ✓ | | Whether the monitor is active |
| `regions` | string[] | ✓ | | Probe regions where checks are executed |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | ✓ | | Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API |
| `managedBy` | string | ✓ | | Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API |
| `createdAt` | string (date-time) | ✓ | | Timestamp when the monitor was created |
| `updatedAt` | string (date-time) | ✓ | | Timestamp when the monitor was last updated |
| `assertions` | MonitorAssertionDto[] | | ✓ | Assertions evaluated against each check result; null on list responses |
Expand All @@ -62,5 +62,5 @@
| `auth` | any | | ✓ | |
| `incidentPolicy` | any | | ✓ | |
| `alertChannelIds` | string (uuid)[] | | ✓ | Alert channel IDs linked to this monitor; populated on single-monitor responses |
| `currentStatus` | "up" \| "degraded" \| "down" \| "paused" \| "unknown" | | ✓ | Current operational state — UP, DOWN, DEGRADED, PAUSED, or UNKNOWN if no probe data yet |
| `currentStatus` | string | | ✓ | Current operational state — UP, DOWN, DEGRADED, PAUSED, or UNKNOWN if no probe data yet |

Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@
| `defaultRetryStrategy` | any | | ✓ | |
| `defaultAlertChannels` | string (uuid)[] | | ✓ | Default alert channel IDs for member monitors |
| `defaultEnvironmentId` | string (uuid) | | ✓ | Default environment ID for member monitors |
| `healthThresholdType` | "COUNT" \| "PERCENTAGE" | | ✓ | Health threshold type: COUNT or PERCENTAGE |
| `healthThresholdType` | string | | ✓ | Health threshold type: COUNT or PERCENTAGE |
| `healthThresholdValue` | number | | ✓ | Health threshold value |
| `suppressMemberAlerts` | boolean | ✓ | | When true, member-level incidents skip notification dispatch; only group alerts fire |
| `confirmationDelaySeconds` | integer (int32) | | ✓ | Seconds to wait after health threshold breach before creating group incident |
| `recoveryCooldownMinutes` | integer (int32) | | ✓ | Cooldown minutes after group incident resolves before a new one can open |
| `health` | ResourceGroupHealthDto | ✓ | | |
| `members` | ResourceGroupMemberDto[] | | ✓ | Member list with individual statuses; populated on detail GET only |
| `managedBy` | "DASHBOARD" \| "CLI" \| "TERRAFORM" \| "MCP" \| "API" | | ✓ | Source that created/owns this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on groups created before this attribution column existed. |
| `managedBy` | string | | ✓ | Source that created/owns this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on groups created before this attribution column existed. |
| `createdAt` | string (date-time) | ✓ | | Timestamp when the group was created |
| `updatedAt` | string (date-time) | ✓ | | Timestamp when the group was last updated |

Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
| `id` | string (uuid) | ✓ | | Unique incident identifier |
| `monitorId` | string (uuid) | | ✓ | Monitor that triggered the incident; null for service or manual incidents |
| `organizationId` | integer (int32) | ✓ | | Organization this incident belongs to |
| `source` | "AUTOMATIC" \| "MANUAL" \| "MONITORS" \| "STATUS_DATA" \| "RESOURCE_GROUP" | ✓ | | Incident origin: MONITOR, SERVICE, or MANUAL |
| `status` | "WATCHING" \| "TRIGGERED" \| "CONFIRMED" \| "RESOLVED" | ✓ | | Current lifecycle status (OPEN, RESOLVED, etc.) |
| `severity` | "DOWN" \| "DEGRADED" \| "MAINTENANCE" | ✓ | | Severity level: DOWN, DEGRADED, or MAINTENANCE |
| `source` | string | ✓ | | Incident origin: MONITOR, SERVICE, or MANUAL |
| `status` | string | ✓ | | Current lifecycle status (OPEN, RESOLVED, etc.) |
| `severity` | string | ✓ | | Severity level: DOWN, DEGRADED, or MAINTENANCE |
| `title` | string | | ✓ | Short summary of the incident; null for auto-generated incidents |
| `triggeredByRule` | string | | ✓ | Human-readable description of the trigger rule that fired |
| `affectedRegions` | string[] | ✓ | | Probe regions that observed the failure |
Expand All @@ -24,7 +24,7 @@
| `externalRef` | string | | ✓ | External reference ID (e.g. PagerDuty incident ID) |
| `affectedComponents` | string[] | | ✓ | Service components affected by this incident |
| `shortlink` | string | | ✓ | Short URL linking to the incident details |
| `resolutionReason` | "MANUAL" \| "AUTO_RECOVERED" \| "AUTO_RESOLVED" | | ✓ | How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.) |
| `resolutionReason` | string | | ✓ | How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.) |
| `startedAt` | string (date-time) | | ✓ | Timestamp when the incident was detected or created |
| `confirmedAt` | string (date-time) | | ✓ | Timestamp when the incident was confirmed (multi-region confirmation) |
| `resolvedAt` | string (date-time) | | ✓ | Timestamp when the incident was resolved |
Expand Down
Loading
Loading