extends: - "spectral:oas" documentationUrl: https://memesio.com/developers/api description: | Spectral ruleset enforcing the conventions observed in Memesio's OpenAPI contract (https://memesio.com/api/openapi). Covers naming, tagging, authentication, AI quota signaling, public/free routes, agent identity, and caption/template content discipline. functions: [] rules: # === Provider info ============================================================ memesio-info-title-required: description: API info.title must include "Memesio". message: '{{property}} should mention "Memesio" — "{{value}}" does not.' severity: warn given: $.info.title then: function: pattern functionOptions: match: "(?i)Memesio" memesio-info-version-required: description: API info.version must be present and follow semver-like format. severity: error given: $.info.version then: function: pattern functionOptions: match: "^\\d+\\.\\d+\\.\\d+(-[A-Za-z0-9.]+)?$" # === Path conventions ========================================================= memesio-paths-must-start-with-api: description: All paths must be served under /api (Memesio routes prefix all surfaces with /api). message: '{{path}} should start with /api/' severity: error given: $.paths.*~ then: function: pattern functionOptions: match: "^/api/" memesio-paths-kebab-case: description: Path segments must use kebab-case (lowercase with hyphens), not snake_case or camelCase. message: 'Path segment in {{path}} should be kebab-case.' severity: warn given: $.paths.*~ then: function: pattern functionOptions: match: "^(/(api|v1|v\\d+)|/[a-z0-9-]+|/\\{[a-zA-Z]+\\})+$" memesio-versioned-paths-under-v1: description: Long-lived versioned routes use /api/v1/* (e.g. /api/v1/agents, /api/v1/memes, /api/v1/templates). message: '{{path}} appears to be a versioned route but does not use /api/v1/.' severity: hint given: $.paths[?(@property.match(/\/api\/v\d+\//) && !@property.startsWith('/api/v1/'))]~ then: function: falsy # === Operation conventions ==================================================== memesio-operation-summary-title-case: description: Operation summaries must be in Title Case. message: 'Summary "{{value}}" should be Title Case.' severity: warn given: $.paths[*][get,post,put,patch,delete].summary then: function: pattern functionOptions: match: "^[A-Z]" memesio-operation-summary-required: description: Every operation must have a non-empty summary. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: summary function: truthy memesio-operation-must-have-tags: description: Every operation must declare at least one tag. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: tags function: truthy memesio-operation-tag-vocabulary: description: Tags should come from the Memesio domain vocabulary. message: 'Tag "{{value}}" is outside the known Memesio tag vocabulary.' severity: hint given: $.paths[*][get,post,put,patch,delete].tags[*] then: function: enumeration functionOptions: values: - agent-infra - ai - ai-captions - ai-jobs - ai-providers - analytics - audio - auth - backend - background-remove - billing - channels - collaboration - compliance - data-eng - data-ops - data-science - developer-api - distribution - experimentation - face-swap - finops - growth - history - lifecycle - marketing-ops - media - memes - moderation - motion - notifications - observability - performance - personalization - platform - product-marketing - public-free - safety - templates - trend-alerts - uploads - video memesio-response-codes-required: description: Operations should declare at least one 2xx response. severity: warn given: $.paths[*][get,post,put,patch,delete].responses then: function: schema functionOptions: schema: type: object patternProperties: "^2\\d{2}$": type: object # === AI / quota routes ======================================================== memesio-ai-routes-grouped: description: AI features live under /api/ai/* or are explicitly tagged "ai". message: '{{path}} looks AI-related but lives outside /api/ai/.' severity: hint given: $.paths[?(@property.match(/(caption|generate|face-swap|background-remove|moderate)/) && !@property.match(/\/api\/ai\//) && !@property.match(/\/free\//))]~ then: function: falsy memesio-ai-quota-get-pairing: description: Mutating AI endpoints under /api/ai/captions and /api/ai/memes should expose a sibling GET for quota inspection. severity: hint given: $.paths[?(@property.match(/\/api\/ai\/(captions|memes)\/generate$/))] then: field: get function: truthy # === Public/free routes ======================================================= memesio-free-routes-tagged-public-free: description: Anonymous routes under /api/free/* must carry the "public-free" tag. severity: error given: $.paths[?(@property.startsWith('/api/free/'))][get,post].tags then: function: schema functionOptions: schema: type: array contains: const: "public-free" # === Agent identity routes ==================================================== memesio-agent-routes-tagged: description: Agent identity and key management routes under /api/v1/agents/* must be tagged "agent-infra" or "developer-api". severity: warn given: $.paths[?(@property.startsWith('/api/v1/agents'))][get,post,patch,delete].tags then: function: schema functionOptions: schema: type: array contains: anyOf: - const: "agent-infra" - const: "developer-api" # === Pagination =============================================================== memesio-list-pagination-shape: description: List endpoints that paginate must expose `page` and `pageSize` query parameters. severity: hint given: $.paths[*].get.parameters then: function: schema functionOptions: schema: type: array memesio-pagesize-bounded: description: pageSize parameters should declare a maximum. severity: warn given: $.paths[*][get].parameters[?(@.name=='pageSize')].schema then: field: maximum function: truthy # === Schemas ================================================================== memesio-schema-names-pascal-case: description: Component schemas must be PascalCase. severity: warn given: $.components.schemas.*~ then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9]+$" memesio-meme-summary-required-fields: description: MemeSummary should expose stable identifiers and a hosted URL. severity: hint given: $.components.schemas.MemeSummary.properties then: function: schema functionOptions: schema: type: object required: [slug]