diff --git a/mintlify-codegen/errors.ts b/mintlify-codegen/errors.ts index 80d1271c9..0d59f7d50 100644 --- a/mintlify-codegen/errors.ts +++ b/mintlify-codegen/errors.ts @@ -282,11 +282,38 @@ function kindSuffix(hasErrors: boolean, hasWarnings: boolean): string { return hasErrors ? 'Errors' : 'Warnings' } -/** Render the full standalone errors/warnings page (frontmatter + sections). */ +/** + * Render a note pointing readers to the errors/warnings shared by all devices. + * Device sub-category pages (locks/thermostats/phones) are subsets that omit the + * device-level codes (`device_offline`, `device_disconnected`, …) applying to + * every device, so a reader troubleshooting connectivity there needs a pointer + * to the aggregate device page. `kinds` is the lowercased sections phrase + * (`errors and warnings` / `errors` / `warnings`) so the prose matches the + * page's actual content. + */ +function renderCommonDeviceErrorsNote( + noun: string, + kinds: string, + route: string, +): string { + const phrase = kinds.toLowerCase() + return [ + '', + ` These are ${noun}-specific ${phrase}. For ${phrase} common to all devices, see [Device Errors and Warnings](${route}).`, + '', + ].join('\n') +} + +/** + * Render the full standalone errors/warnings page (frontmatter + sections). When + * `commonDeviceErrorsRoute` is set, a note linking to the shared device + * errors/warnings page is prepended — used on device sub-category pages. + */ function renderPage( noun: string, errorSection: string, warningSection: string, + commonDeviceErrorsRoute?: string, ): string { const title = `${noun} ${kindSuffix(Boolean(errorSection), Boolean(warningSection))}` const kinds = @@ -302,7 +329,10 @@ function renderPage( `description: '${description.replace(/'/g, "\\'")}'`, '---', ].join('\n') - const body = [errorSection, warningSection].filter(Boolean).join('\n\n') + const note = commonDeviceErrorsRoute + ? renderCommonDeviceErrorsNote(noun, kinds, commonDeviceErrorsRoute) + : '' + const body = [note, errorSection, warningSection].filter(Boolean).join('\n\n') return `${frontmatter}\n\n${body}\n` } @@ -314,6 +344,13 @@ async function readFileOrNull(path: string): Promise { } } +interface ErrorPageOptions { + // When set, prepend a note linking to the errors/warnings shared by all + // devices. Used only for device sub-category pages (locks/thermostats/phones), + // which are subsets that omit the device-level codes. + commonDeviceErrorsRoute?: string +} + /** * Write one resource's `errors.mdx` (an `## Errors` and/or `## Warnings` * section) and record its route in `routes`. No-ops when the resource has no @@ -325,7 +362,16 @@ async function writeErrorPage( errorsProp: Property | undefined, warningsProp: Property | undefined, routes: string[], + options: ErrorPageOptions = {}, ): Promise { + // Defensive against a variant-group key ever coinciding with a real resource + // route: the resource loop writes first, so skip a route already emitted + // rather than clobbering it from the sub-category loop. + if (routes.includes(routePath)) { + console.log(` Errors page for ${routePath} already written, skipping`) + return + } + const errorGroups = groupCodes(errorsProp) const warningGroups = groupCodes(warningsProp) if (errorGroups.length === 0 && warningGroups.length === 0) return @@ -342,11 +388,24 @@ async function writeErrorPage( noun, renderSection('Errors', 'error', errorsProp, errorGroups), renderSection('Warnings', 'warning', warningsProp, warningGroups), + options.commonDeviceErrorsRoute, ) await writeFile(join(resourceDir, 'errors.mdx'), page) routes.push(routePath) } +interface DeviceSubcategory { + // Route that carries the sub-category's dedicated errors page. + routePath: string + // Whether the sub-category shares the device-level errors/warnings documented + // on /api/devices/errors, and so should cross-link to them. The shared codes + // are physical-device connectivity/subscription errors (`device_offline`, + // `hub_disconnected`, `salto_ks_subscription_limit_exceeded`, …), relevant to + // locks and thermostats but not to phones (mobile devices used for + // credentials), so the phones page omits the cross-link. + linkCommonDeviceErrors: boolean +} + // Some device sub-categories have their own docs route but no blueprint // resource of their own — the Seam API models a lock, thermostat, or phone as a // `device`. Their error/warning codes therefore live on the `device` resource @@ -359,12 +418,17 @@ async function writeErrorPage( // but have no standalone sub-category route. In particular /access_codes // already documents the `access_code` resource's own, distinct errors — routing // the device `access_codes` group there would clobber it. -const DEVICE_SUBCATEGORY_ERROR_ROUTES: Record = { - locks: '/locks', - phones: '/phones', - thermostats: '/thermostats', +const DEVICE_SUBCATEGORY_ERROR_ROUTES: Record = { + locks: { routePath: '/locks', linkCommonDeviceErrors: true }, + phones: { routePath: '/phones', linkCommonDeviceErrors: false }, + thermostats: { routePath: '/thermostats', linkCommonDeviceErrors: true }, } +// Route of the aggregate device errors/warnings page. Sub-categories that share +// the device-level codes link back to it (see writeErrorPage's +// `commonDeviceErrorsRoute` option). +const COMMON_DEVICE_ERRORS_ROUTE = '/api/devices/errors' + /** * Narrow a device `errors`/`warnings` property to a single variant group, * flattening the kept variants to ungrouped (`variantGroupKey` cleared) so their @@ -384,6 +448,58 @@ function variantGroupProp( return { ...prop, variants, variantGroups: [] } } +/** + * Warn when a device errors/warnings variant group looks like it should have its + * own sub-category errors page but is missing from DEVICE_SUBCATEGORY_ERROR_ROUTES. + * + * That allowlist is maintained by hand, so a newly added device sub-category + * (its own object + events pages plus an error/warning variant group) would + * silently produce no errors page until someone edits the map. This surfaces + * that inconsistency at generate time. + * + * A group is flagged only when it (a) has documented codes, (b) has both an + * `object.mdx` and an `events.mdx` under `/api/` — the shape a real + * sub-category route has — and (c) is neither already mapped nor itself a + * documented blueprint resource. Condition (c) keeps real resources + * (`access_codes`, which owns its own distinct errors) and annotation-only + * groups (`hardware`, `provider_metadata` — no such route) from tripping it. + */ +async function warnUnmappedDeviceSubcategoryGroups( + docsDir: string, + errorsProp: Property | undefined, + warningsProp: Property | undefined, + documentedResourceRoutes: Set, +): Promise { + const groupKeys = new Set() + for (const prop of [errorsProp, warningsProp]) { + if (!isDiscriminatedListProperty(prop)) continue + for (const group of prop.variantGroups) groupKeys.add(group.variantGroupKey) + } + + for (const groupKey of groupKeys) { + if (groupKey in DEVICE_SUBCATEGORY_ERROR_ROUTES) continue + if (documentedResourceRoutes.has(`/${groupKey}`)) continue + + const hasCodes = + groupCodes(variantGroupProp(errorsProp, groupKey)).length > 0 || + groupCodes(variantGroupProp(warningsProp, groupKey)).length > 0 + if (!hasCodes) continue + + const resourceDir = join(docsDir, 'api', groupKey) + const hasObject = + (await readFileOrNull(join(resourceDir, 'object.mdx'))) != null + const hasEvents = + (await readFileOrNull(join(resourceDir, 'events.mdx'))) != null + if (!hasObject || !hasEvents) continue + + console.log( + ` WARNING: device errors/warnings variant group "${groupKey}" has codes ` + + `and an object+events page (/api/${groupKey}) but no dedicated errors ` + + `page. Add it to DEVICE_SUBCATEGORY_ERROR_ROUTES in mintlify-codegen/errors.ts.`, + ) + } +} + /** * Generate the per-resource `errors.mdx` pages. Returns the route paths that * received a page (e.g. `/devices`) so the caller can wire them into the @@ -413,17 +529,35 @@ export async function updateErrorPages( if (device != null) { const errorsProp = device.properties.find((p) => p.name === 'errors') const warningsProp = device.properties.find((p) => p.name === 'warnings') - for (const [groupKey, routePath] of Object.entries( - DEVICE_SUBCATEGORY_ERROR_ROUTES, - )) { + for (const [ + groupKey, + { routePath, linkCommonDeviceErrors }, + ] of Object.entries(DEVICE_SUBCATEGORY_ERROR_ROUTES)) { await writeErrorPage( docsDir, routePath, variantGroupProp(errorsProp, groupKey), variantGroupProp(warningsProp, groupKey), routes, + linkCommonDeviceErrors + ? { commonDeviceErrorsRoute: COMMON_DEVICE_ERRORS_ROUTE } + : {}, ) } + + // Flag any device sub-category that has its own object+events pages and + // error codes but was never added to the allowlist above. + const documentedResourceRoutes = new Set( + blueprint.resources + .filter((r) => !r.isUndocumented) + .map((r) => r.routePath), + ) + await warnUnmappedDeviceSubcategoryGroups( + docsDir, + errorsProp, + warningsProp, + documentedResourceRoutes, + ) } return routes diff --git a/mintlify-docs/api/locks/errors.mdx b/mintlify-docs/api/locks/errors.mdx index 3ab463a6f..cad40bcd9 100644 --- a/mintlify-docs/api/locks/errors.mdx +++ b/mintlify-docs/api/locks/errors.mdx @@ -3,6 +3,10 @@ title: 'Lock Errors and Warnings' description: 'Errors and warnings that Seam reports on the Lock resource, each with its code and meaning.' --- + + These are Lock-specific errors and warnings. For errors and warnings common to all devices, see [Device Errors and Warnings](/api/devices/errors). + + ## Errors Each error is an object with the following shape: diff --git a/mintlify-docs/api/thermostats/errors.mdx b/mintlify-docs/api/thermostats/errors.mdx index 6d6e9357f..bd0c0837c 100644 --- a/mintlify-docs/api/thermostats/errors.mdx +++ b/mintlify-docs/api/thermostats/errors.mdx @@ -3,6 +3,10 @@ title: 'Thermostat Errors and Warnings' description: 'Errors and warnings that Seam reports on the Thermostat resource, each with its code and meaning.' --- + + These are Thermostat-specific errors and warnings. For errors and warnings common to all devices, see [Device Errors and Warnings](/api/devices/errors). + + ## Errors Each error is an object with the following shape: