Skip to content

Commit 0511341

Browse files
fix(rest): openapi.json documents a valid declared endpoint the served read decorated (#23083) (#23087)
Fixes #23083 Clause-②: no ## What was wrong `GET /api/v1/openapi.json` folds the declared `api` items into the document through `selectDocumentableEndpoints` (`packages/rest/src/openapi-endpoints.ts`). Where no endpoint matcher is reachable, the handler passes it the items `protocol.getMetaItems({ type: 'api' })` serves. That read exits through `decorateMetadataItems`, which stamps `_diagnostics` on every `api` item. `ApiEndpointSchema` is closed, so the reader refused the platform's own badge by name (`unrecognized_keys: ["_diagnostics"]`): every valid declaration was left out of the document and logged at `error` as a declaration to fix. ## The change - `selectDocumentableEndpoints` runs `stripReadDecorations` (`@objectstack/spec/kernel`) before `ApiEndpointSchema.safeParse`. It removes exactly `METADATA_READ_DECORATIONS` (`_diagnostics`, `_draft`), the same strip the seed door applies. There is no local key list and no blanket underscore strip, and the schema is unchanged. - The ADR-0010 envelope keys (`_packageId`, `_provenance`, and the rest) are allowlisted by the schema and parse exactly as before. Any other unknown key is still refused. - A truly invalid item is still omitted and logged at `error`. The log line's second sentence ("The endpoint matcher excludes it too, so the route it declares answers 404") is gone, because it was measured false (below). The line now reads: `[REST] stored api item 'NAME' does not satisfy ApiEndpointSchema — it is OMITTED from the OpenAPI document; fix the declaration rather than the document.` - Changeset: `@objectstack/rest` patch, carrying `Clause-②: no`. ## Reach, measured on this tree (base `62ce45d140`) The caller is the `GET {basePath}/openapi.json` handler, step 4 (`rest-server.ts`). Its read is `protocol.getMetaItems({ type: 'api' })`, served audience. The stamping site is `decorateMetadataItems` (`packages/metadata-protocol/src/protocol.ts`, the `audience === 'served'` exit). The raw items reach the reader only when `resolveEndpointMatchAuthority` finds no `metadata` service with `matchEndpoint`. With a matcher, the reader gets the matcher's own parsed endpoints, which carry no decoration, and the strip is a no-op that returns the same reference. | composition | metadata slot has `matchEndpoint` | declared paths in `openapi.json` | omission lines at `error` | |:--|:--|:--|:--| | `@objectstack/verify` `bootStack(showcase)`, before | no (kernel in-memory fallback) | 0 of 2 | 2 | | same, after | no | 2 of 2 | 0 | | `os serve objectstack.config.ts` (showcase), after (CONTROL) | yes (`MetadataPlugin` since #23078) | 2 of 2 | 0 | - On `bootStack`, `GET /api/v1/meta/api` answers both items with keys `_packageId`, `_packageVersion`, `_provenance` and `_diagnostics: {"valid":true}`. - On both runs `bootStack` still logs the once-per-server notice "no endpoint matcher is reachable … which may advertise routes that answer 404". That is a different line, owned by `rest-server.ts`. - On `bootStack` the declared route itself answers `GET /api/v1/apps/showcase/tasks` with `404 ENDPOINT_NOT_FOUND`, because nothing serves declared endpoints without a matcher. The document now lists both routes there, which is the stored-items fallback that notice describes. On `os serve` the same route answers `200`. - `_draft` does not reach this reader today: the handler passes no preview mode to `getMetaItems`. The `_draft` pin covers robustness only. Before, `bootStack`, one of the two lines: ```text [REST] stored api item 'showcase_task_feed' does not satisfy ApiEndpointSchema — it is OMITTED from the OpenAPI document. The endpoint matcher excludes it too, so the route it declares answers 404; fix the declaration rather than the document. {"issues":[{"code":"unrecognized_keys","keys":["_diagnostics"],"path":[], …}]} ``` After, `bootStack`: no line names either endpoint. ## The matcher sentence, measured The real `MetadataManager` holds the STORED item, and `matchEndpoint` is asked. The reader gets what the openapi.json caller hands it. Both measurements were taken on this tree before the change: | case | matcher serves | reader documents (before) | reader's refusal | |:--|:--|:--|:--| | A: valid item through `decorateMetadataItems` | yes | no | `unrecognized_keys: _diagnostics` | | B: valid item with `_draft: true` | yes | no | `unrecognized_keys: _draft` | | C: truly invalid (`type: 'nope'`) | no | no | `invalid_value: type` | | D: valid body in a flat stored envelope (`packageId`, `state`) | yes | no | `unrecognized_keys: packageId,state` | | E: ADR-0010 `_packageId` | yes | yes | none | The sentence held only in case C. The matcher peels the storage envelope before it parses, and on the composition where this reader sees raw items there is no matcher at all. After the change, A and B are documented, and C and D are still refused. ## Tests, at `76f507c0ee` - New file `packages/rest/src/openapi-endpoints-read-decorations.test.ts`, 7 cases. Its fixtures are the real producer's output (`decorateMetadataItems` from `@objectstack/metadata-protocol`). - Pins: two valid items decorated by the served read are documented and nothing is logged; an item carrying `_draft` is documented; both decorated items become path entries; and `GET /openapi.json` with no matcher wired lists both endpoints, with no `console.error` line naming either. - CONTROLS: a decorated, truly invalid item is still omitted and logged, and its issues name `type` with no `unrecognized_keys`; `_packageId` and `_provenance` parse to exactly `ApiEndpointSchema.parse(item)`, decorated or not; `_unexpected` is still refused as `unrecognized_keys`. - `pnpm --filter @objectstack/rest test`: `Test Files 275 passed (275)`, `Tests 5758 passed | 326 skipped (6084)`, lock `VERDICT command-exit 0`. - `pnpm --filter @objectstack/rest typecheck`: exit 0. `check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s)`. `tsc -p tsconfig.test.json --listFiles` includes the new file, 0 errors. - Ablation, committed first. `scripts/ablation-replace.mjs` replaced `ApiEndpointSchema.safeParse(stripReadDecorations(item))` with `ApiEndpointSchema.safeParse(item)`: anchor 1 to 0, blob `45e2bea3bfc8` to `258febe723a5`. It ran inside the verify lock under an EXIT/INT/TERM restore trap. No build was needed, because the tests import the subject from `src` by relative path. The direction was predicted before the run: 6 red, the `_unexpected` control green, the existing file green. Observed: `Tests 6 failed | 27 passed (33)`, exactly those 6. Restore: blob `45e2bea3bfc8` equals HEAD and `git diff HEAD` is empty. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 62 commands, identical with and without explicit paths. All 62 exit 0. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`, 14 packages had no `dist/`) and exited 0 after the cli closure was built. `--ran`: `62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED`. - Lint, by proven narrowing (CI runs `pnpm lint`): - eslint `--no-inline-config --format json` over the 2 touched `.ts` files: 2 files, 0 errors, 0 warnings. - `--print-config` gives both files a config (6 and 5 rules). The changeset `.md` is outside eslint's population (`undefined`). - `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no `projectService`), and this diff touches none of its inputs. So no untouched file's verdict can move. ## Acceptance notes - Not filed, carrier none: case D above. A valid body in a flat stored envelope (`packageId` / `state`) is refused by this reader while the matcher serves it, and the log still says to fix the declaration. No producer that hands this reader such rows was found, so its reach is unmeasured. - Not filed, carrier: the next PR to touch this file. The module docblock "What it is HANDED" says the production caller passes the matcher-confirmed set. That holds only when a matcher is reachable. Without one, it passes the served items, which is the path this change repairs. - This branch is one commit behind `origin/main` (`0aef788816`, `plugin-hono-server` only, disjoint from this diff). It is not merged. - Not run locally: `pnpm --filter @objectstack/rest test:repo`. It is declared to CI. --- _Generated by [Claude Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 968acc8 commit 0511341

3 files changed

Lines changed: 277 additions & 3 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): `GET /api/v1/openapi.json` documents a valid declared endpoint that the served read decorated, and stops logging it as a declaration to fix (#23083)
6+
7+
Clause-②: no
8+
9+
**What was wrong.** On a composition with no endpoint matcher (no `metadata` service with `matchEndpoint`, for example `@objectstack/verify`'s `bootStack`), the document is built from the `api` items `getMetaItems` serves. That read adds `_diagnostics` to every item, and `ApiEndpointSchema` is closed, so the document's own parse refused every valid declaration by that key. The endpoints were left out of the document. For each one, the server logged at `error` that the declaration did not satisfy `ApiEndpointSchema` and should be fixed. Measured on the showcase booted through `bootStack`: 0 of its 2 declared endpoints in the document, and 2 such `error` lines.
10+
11+
**What changes.**
12+
13+
- The document's parse first removes the read-time decorations (`METADATA_READ_DECORATIONS` from `@objectstack/spec/kernel`: `_diagnostics`, `_draft`). It is the same strip the seed door applies. On that boot the document now lists both endpoints, and no line about them is logged.
14+
- The ADR-0010 envelope keys (`_packageId`, `_provenance`, …) are allowlisted by the schema and parse as before. Any other unknown key is still refused.
15+
- A declaration that really is invalid is still left out of the document and logged at `error`. The log line no longer says that the endpoint matcher excludes the item too. The document's parse does not know that: the matcher judges its own stored copy after removing the storage envelope, and where no matcher is reachable there is none to ask.
16+
17+
**Upgrading.** Nothing to change. `ApiEndpointSchema` is unchanged.
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #23083 — the OpenAPI reader re-parses a SERVED `api` item, so it strips the
5+
* read-time decorations before its closed parse.
6+
*
7+
* `GET /openapi.json` enumerates `api` items through `protocol.getMetaItems`,
8+
* whose served audience exits through `decorateMetadataItems` and stamps
9+
* `_diagnostics` on every item (`api` has a registered schema). Where no
10+
* endpoint matcher is reachable — a composition whose `metadata` slot is the
11+
* kernel's in-memory fallback, `@objectstack/verify`'s `bootStack` among them —
12+
* the handler hands those served items to `selectDocumentableEndpoints` as they
13+
* are, and the closed `ApiEndpointSchema` refused the platform's own badge by
14+
* name, `unrecognized_keys: ["_diagnostics"]`: every VALID declaration was
15+
* omitted from the document and logged at `error` as a declaration to fix.
16+
*
17+
* The fixtures are the platform's own output: `decorateMetadataItems` from
18+
* `@objectstack/metadata-protocol` is the producer the served read calls, so a
19+
* decoration it stamps is in the fixture without anyone editing this file.
20+
*
21+
* Controls, each the other half of a pin above it:
22+
* - a truly invalid item is still omitted and logged — the strip launders
23+
* nothing, and the refusal names the item's real defect;
24+
* - the ADR-0010 envelope (`_packageId`, `_provenance`) parses exactly as it
25+
* did — the closed schema allowlists it, so it is not a decoration;
26+
* - an underscore key that is NOT a declared read decoration is still refused:
27+
* the strip is `METADATA_READ_DECORATIONS`, never a blanket `_` prefix.
28+
*/
29+
30+
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
31+
import { ApiEndpointSchema } from '@objectstack/spec/api';
32+
import { decorateMetadataItems } from '@objectstack/metadata-protocol';
33+
import { enrichOpenApiWithEndpoints, selectDocumentableEndpoints } from './openapi-endpoints';
34+
import { RestServer } from './rest-server';
35+
36+
// [#17865] The handler-level pin below observes the REST fault log, so this
37+
// file declares the level it asserts against instead of inheriting the suite's
38+
// quiet one. 'info' is the SHIPPED default — what a real caller gets.
39+
beforeAll(() => { vi.stubEnv('OS_REST_LOG', 'info'); });
40+
afterAll(() => { vi.unstubAllEnvs(); });
41+
afterEach(() => { vi.restoreAllMocks(); });
42+
43+
// ---------------------------------------------------------------------------
44+
// The showcase's two declarations, restated (not imported — an example app is
45+
// not this package's dependency)
46+
// ---------------------------------------------------------------------------
47+
48+
const TASK_FEED = {
49+
name: 'showcase_task_feed',
50+
path: '/api/v1/apps/showcase/tasks',
51+
method: 'GET',
52+
summary: 'Task feed',
53+
type: 'object_operation',
54+
objectParams: { object: 'showcase_task', operation: 'find' },
55+
authRequired: true,
56+
cacheTtlSeconds: 30,
57+
};
58+
59+
const INQUIRY_PURGE = {
60+
name: 'showcase_inquiry_purge_api',
61+
path: '/api/v1/apps/showcase/inquiries/purge',
62+
method: 'POST',
63+
summary: 'Purge closed inquiries',
64+
type: 'flow',
65+
target: 'showcase_inquiry_purge',
66+
authRequired: true,
67+
};
68+
69+
/** What the served read hands a reader: the producer's own decoration. */
70+
function servedRead(items: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
71+
return decorateMetadataItems('api', items.map((item) => ({ ...item })));
72+
}
73+
74+
function collectingLogger() {
75+
return { error: vi.fn() };
76+
}
77+
78+
function baseDoc() {
79+
return {
80+
openapi: '3.1.0',
81+
info: { title: 'ObjectStack API', version: '17.0.0' },
82+
paths: { '/api/meta': { get: { operationId: 'getMeta' } } },
83+
components: { schemas: {}, securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } } },
84+
security: [{ bearerAuth: [] }],
85+
};
86+
}
87+
88+
// ---------------------------------------------------------------------------
89+
// Pins
90+
// ---------------------------------------------------------------------------
91+
92+
describe('a valid declaration is documented however the read decorated it (#23083)', () => {
93+
it('documents both items exactly as the served read decorates them, and logs nothing', () => {
94+
const served = servedRead([TASK_FEED, INQUIRY_PURGE]);
95+
// Precondition: the fixture really carries the decoration, and the producer
96+
// itself calls both items valid — so a refusal here can only be the reader's.
97+
for (const item of served) expect(item._diagnostics).toEqual({ valid: true });
98+
99+
const logger = collectingLogger();
100+
const kept = selectDocumentableEndpoints(served, logger);
101+
102+
expect(kept.map((e) => e.name).sort()).toEqual(['showcase_inquiry_purge_api', 'showcase_task_feed']);
103+
expect(logger.error).not.toHaveBeenCalled();
104+
for (const endpoint of kept) expect(endpoint).not.toHaveProperty('_diagnostics');
105+
});
106+
107+
it('documents an item carrying the draft badge, and logs nothing', () => {
108+
const logger = collectingLogger();
109+
const kept = selectDocumentableEndpoints([{ ...TASK_FEED, _draft: true }], logger);
110+
111+
expect(kept.map((e) => e.name)).toEqual(['showcase_task_feed']);
112+
expect(logger.error).not.toHaveBeenCalled();
113+
expect(kept[0]).not.toHaveProperty('_draft');
114+
});
115+
116+
it('folds the decorated declarations into the document as path entries', () => {
117+
const logger = collectingLogger();
118+
const out = enrichOpenApiWithEndpoints(baseDoc(), servedRead([TASK_FEED, INQUIRY_PURGE]), logger) as any;
119+
120+
expect(out.paths['/api/v1/apps/showcase/tasks'].get.operationId).toBe('showcase_task_feed');
121+
expect(out.paths['/api/v1/apps/showcase/inquiries/purge'].post.operationId).toBe('showcase_inquiry_purge_api');
122+
expect(logger.error).not.toHaveBeenCalled();
123+
});
124+
});
125+
126+
describe('controls — the strip removes the declared decorations and nothing else', () => {
127+
it('still omits and logs a truly invalid item, naming its real defect', () => {
128+
const served = servedRead([{ ...TASK_FEED, name: 'bad_type', type: 'nope' }]);
129+
// The producer agrees it is invalid — the decoration rides on a bad item.
130+
expect((served[0]._diagnostics as { valid: boolean }).valid).toBe(false);
131+
132+
const logger = collectingLogger();
133+
const kept = selectDocumentableEndpoints(served, logger);
134+
135+
expect(kept).toEqual([]);
136+
expect(logger.error).toHaveBeenCalledTimes(1);
137+
expect(logger.error.mock.calls[0][0]).toContain("'bad_type'");
138+
const { issues } = logger.error.mock.calls[0][1] as { issues: Array<{ code: string; path: unknown[]; keys?: string[] }> };
139+
// The refusal is about the item, not about our own badge.
140+
expect(issues.some((i) => i.path[0] === 'type')).toBe(true);
141+
expect(issues.some((i) => i.code === 'unrecognized_keys')).toBe(false);
142+
});
143+
144+
it('parses the ADR-0010 envelope exactly as before, decorated or not', () => {
145+
const enveloped = { ...TASK_FEED, _packageId: 'com.example.showcase', _provenance: 'package' };
146+
const expected = ApiEndpointSchema.parse(enveloped);
147+
148+
const logger = collectingLogger();
149+
const [plain] = selectDocumentableEndpoints([enveloped], logger);
150+
const [decorated] = selectDocumentableEndpoints(servedRead([enveloped]), logger);
151+
152+
expect(plain).toEqual(expected);
153+
expect(decorated).toEqual(expected);
154+
expect(decorated._packageId).toBe('com.example.showcase');
155+
expect(logger.error).not.toHaveBeenCalled();
156+
});
157+
158+
it('still refuses an underscore key that is not a declared read decoration', () => {
159+
const logger = collectingLogger();
160+
const kept = selectDocumentableEndpoints([{ ...TASK_FEED, _unexpected: true }], logger);
161+
162+
expect(kept).toEqual([]);
163+
expect(logger.error).toHaveBeenCalledTimes(1);
164+
const { issues } = logger.error.mock.calls[0][1] as { issues: Array<{ code: string; keys?: string[] }> };
165+
expect(issues.find((i) => i.code === 'unrecognized_keys')?.keys).toEqual(['_unexpected']);
166+
});
167+
});
168+
169+
// ---------------------------------------------------------------------------
170+
// The production caller, on a composition with no endpoint matcher
171+
// ---------------------------------------------------------------------------
172+
173+
function createMockServer() {
174+
return {
175+
get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), use: vi.fn(),
176+
listen: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined),
177+
};
178+
}
179+
180+
function makeRes() {
181+
const res: any = { statusCode: 200, body: undefined };
182+
res.status = vi.fn((c: number) => { res.statusCode = c; return res; });
183+
res.json = vi.fn((b: any) => { res.body = b; return res; });
184+
res.header = vi.fn(() => res); res.setHeader = vi.fn(); res.write = vi.fn(); res.end = vi.fn(); res.send = vi.fn();
185+
return res;
186+
}
187+
188+
/**
189+
* The REST server with NO metadata service wired — the shape of a composition
190+
* whose `metadata` slot cannot answer `matchEndpoint` — over a protocol whose
191+
* `getMetaItems` answers what the served read answers.
192+
*/
193+
function mountWithoutMatcher(served: Array<Record<string, unknown>>) {
194+
const protocol: any = {
195+
getDiscovery: vi.fn().mockResolvedValue({
196+
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
197+
}),
198+
getMetaTypes: vi.fn().mockResolvedValue([]),
199+
getMetaItems: vi.fn(async ({ type }: any) => {
200+
const t = String(type ?? '');
201+
if (t === 'api' || t === 'apis') return { type: 'api', items: served };
202+
return { type: t, items: [] };
203+
}),
204+
getMetaItem: vi.fn().mockResolvedValue({}),
205+
findData: vi.fn().mockResolvedValue([]),
206+
};
207+
const rest = new RestServer(
208+
createMockServer() as any,
209+
protocol,
210+
{ api: { requireAuth: false, version: 'v1' } } as any,
211+
);
212+
// The description document denies anonymous callers; an identity is a
213+
// precondition of reaching the handler, not the subject here.
214+
(rest as any).resolveExecCtx = async () => ({ userId: 'u1' });
215+
rest.registerRoutes();
216+
(rest as any).registerOpenApiEndpoints('/api/v1');
217+
return rest;
218+
}
219+
220+
async function getOpenApi(rest: any) {
221+
const entry = rest.routeManager.get('GET', '/api/v1/openapi.json');
222+
if (!entry) throw new Error('openapi.json route not registered');
223+
const res = makeRes();
224+
await entry.handler({ method: 'GET', headers: { host: 'example.test' }, params: {}, query: {}, path: '/api/v1/openapi.json' }, res);
225+
return res;
226+
}
227+
228+
describe('GET /openapi.json with no endpoint matcher reachable', () => {
229+
it('lists both declared endpoints, and no error line names either of them', async () => {
230+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
231+
const rest = mountWithoutMatcher(servedRead([TASK_FEED, INQUIRY_PURGE]));
232+
233+
const res = await getOpenApi(rest);
234+
235+
expect(res.statusCode).toBe(200);
236+
const declared = Object.keys(res.body?.paths ?? {}).filter((p) => p.startsWith('/api/v1/apps/')).sort();
237+
expect(declared).toEqual(['/api/v1/apps/showcase/inquiries/purge', '/api/v1/apps/showcase/tasks']);
238+
239+
const lines = errorSpy.mock.calls.map((args) => args.map(String).join(' '));
240+
expect(lines.filter((l) => l.includes('showcase_task_feed') || l.includes('showcase_inquiry_purge_api'))).toEqual([]);
241+
}, 60_000);
242+
});

‎packages/rest/src/openapi-endpoints.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
*/
9191

9292
import { ApiEndpointSchema, type ApiEndpoint } from '@objectstack/spec/api';
93+
import { stripReadDecorations } from '@objectstack/spec/kernel';
9394

9495
/** The slice of an OpenAPI document this module reads and rewrites. */
9596
export interface OpenApiDocumentLike {
@@ -317,6 +318,17 @@ export function buildEndpointOperation(
317318
* was chosen there: it is total, deterministic across nodes and boots, and it
318319
* must produce the SAME winner, or the document would name an endpoint the
319320
* runtime does not run.
321+
*
322+
* The items may be SERVED documents, so the read-time decorations come off
323+
* before the closed parse (#23083). Where no endpoint matcher is reachable the
324+
* caller hands over `protocol.getMetaItems` output as-is, and that read stamps
325+
* `_diagnostics` on every `api` item: `ApiEndpointSchema` is closed, so it
326+
* refused the platform's own badge by name and every valid declaration was
327+
* omitted and logged as one to fix. The strip is the spec's declared list
328+
* (`METADATA_READ_DECORATIONS`, the same one the seed door strips for #15591),
329+
* ⛔ never a blanket `_` prefix: the ADR-0010 envelope (`_packageId`,
330+
* `_provenance`, …) is allowlisted by the schema and parses as it always did,
331+
* and any other unknown key is still refused.
320332
*/
321333
export function selectDocumentableEndpoints(
322334
items: readonly unknown[],
@@ -325,16 +337,19 @@ export function selectDocumentableEndpoints(
325337
const byRoute = new Map<string, ApiEndpoint>();
326338

327339
for (const item of items) {
328-
const parsed = ApiEndpointSchema.safeParse(item);
340+
const parsed = ApiEndpointSchema.safeParse(stripReadDecorations(item));
329341
if (!parsed.success) {
330342
const declaredName =
331343
item && typeof item === 'object' && typeof (item as { name?: unknown }).name === 'string'
332344
? (item as { name: string }).name
333345
: '<unnamed>';
346+
// Says only what this reader knows. It does not speak for the endpoint
347+
// matcher: that reader judges its own stored item after peeling the
348+
// storage envelope, so it can serve what this parse refuses — and where
349+
// the caller found no matcher to ask, there is none to speak for.
334350
logger.error(
335351
`[REST] stored api item '${declaredName}' does not satisfy ApiEndpointSchema — it is OMITTED ` +
336-
`from the OpenAPI document. The endpoint matcher excludes it too, so the route it declares ` +
337-
`answers 404; fix the declaration rather than the document.`,
352+
`from the OpenAPI document; fix the declaration rather than the document.`,
338353
{ issues: parsed.error.issues },
339354
);
340355
continue;

0 commit comments

Comments
 (0)