Skip to content

Commit 4c51bbb

Browse files
louis-preclaude
andcommitted
Throw if documented endpoints reference undocumented resources
Fail at build time when a documented endpoint's response references an undocumented resource type. This prevents generating broken links to pages that don't exist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent de677e1 commit 4c51bbb

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

src/lib/blueprint.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ export const createBlueprint = async (
510510
actionAttempts,
511511
})
512512

513+
assertDocumentedEndpointsDontReferenceUndocumentedResources({
514+
routes,
515+
resources,
516+
})
517+
513518
return {
514519
title: openapi.info.title,
515520
routes,
@@ -577,6 +582,38 @@ const assertSeamPathsAreUndocumented = ({
577582
}
578583
}
579584

585+
const assertDocumentedEndpointsDontReferenceUndocumentedResources = ({
586+
routes,
587+
resources,
588+
}: Pick<Blueprint, 'routes' | 'resources'>): void => {
589+
const undocumentedResourceTypes = new Set(
590+
resources.filter((r) => r.isUndocumented).map((r) => r.resourceType),
591+
)
592+
593+
const offenders: string[] = []
594+
595+
for (const route of routes) {
596+
for (const endpoint of route.endpoints) {
597+
if (endpoint.isUndocumented) continue
598+
if (endpoint.response.responseType === 'void') continue
599+
if (!('resourceType' in endpoint.response)) continue
600+
601+
const { resourceType } = endpoint.response
602+
if (undocumentedResourceTypes.has(resourceType)) {
603+
offenders.push(
604+
`${endpoint.path} references undocumented resource '${resourceType}'`,
605+
)
606+
}
607+
}
608+
}
609+
610+
if (offenders.length > 0) {
611+
throw new Error(
612+
`Documented endpoints must not reference undocumented resources. Found:\n${offenders.join('\n')}`,
613+
)
614+
}
615+
}
616+
580617
const extractValidActionAttemptTypes = (
581618
schemas: Openapi['components']['schemas'],
582619
): string[] => {

test/blueprint.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,55 @@ test('createBlueprint: throws when a /seam entry is documented', async (t) => {
5353
/All \/seam entries must be marked undocumented\. Found: .*\/seam\/widgets\/get/,
5454
})
5555
})
56+
57+
test('createBlueprint: throws when a documented endpoint references an undocumented resource', async (t) => {
58+
const typesModule = TypesModuleSchema.parse(types)
59+
const openapi = structuredClone(typesModule.openapi)
60+
61+
// Add an undocumented resource
62+
openapi.components.schemas['secret_widget'] = {
63+
type: 'object',
64+
properties: {
65+
secret_widget_id: { type: 'string', format: 'uuid' },
66+
},
67+
required: ['secret_widget_id'],
68+
'x-undocumented': 'This resource is not yet public.',
69+
'x-route-path': '/foos',
70+
}
71+
72+
// Add a documented endpoint that returns the undocumented resource
73+
openapi.paths['/widgets/get'] = {
74+
post: {
75+
operationId: 'widgetsGetPost',
76+
responses: {
77+
200: {
78+
content: {
79+
'application/json': {
80+
schema: {
81+
properties: {
82+
ok: { type: 'boolean' },
83+
secret_widget: {
84+
$ref: '#/components/schemas/secret_widget',
85+
},
86+
},
87+
required: ['ok', 'secret_widget'],
88+
type: 'object',
89+
},
90+
},
91+
},
92+
description: 'OK',
93+
},
94+
},
95+
security: [],
96+
summary: '/widgets/get',
97+
tags: ['/widgets'],
98+
'x-response-key': 'secret_widget',
99+
'x-title': 'Get a widget',
100+
},
101+
}
102+
103+
await t.throwsAsync(() => createBlueprint({ ...typesModule, openapi }), {
104+
message:
105+
/Documented endpoints must not reference undocumented resources\. Found:\n.*\/widgets\/get.*secret_widget/,
106+
})
107+
})

0 commit comments

Comments
 (0)