pediatric-ai-scribe-v3/src/utils/openapiRoutes.js
Daniel 993442f98a feat: the API describes itself, at /api/openapi.json
docs/api-reference.md was hand-written, and by the time anyone checked
it was documenting twenty-three endpoints that answer 404 while missing
others that exist. That is what hand-written reference material does: it
is correct on the day it is written and silently wrong afterwards. A
second hand-written document, in YAML this time, would rot the same way.

So paths, methods and mount points are read from the Express router
stack at request time. They cannot disagree with the app, because they
are the app: 186 paths, 215 operations, and — checked — no /learning
endpoints, which is what the prose version went on claiming for weeks
after that feature was deleted.

What introspection cannot know is what an endpoint is *for*. That half
lives in src/utils/openapiRoutes.js, keyed by "METHOD /path", and it is
the half that rots, so it is the half that is enforced: a Playwright
spec fetches the live document and fails when the number of operations
without a summary rises above 199 — the debt as measured today. A
ratchet, not a target. Adding an endpoint pushes the count over and
fails the build; describing one lowers the number. The failure lists the
operations by name, so it says what to write.

Whether an operation is public is stated per route rather than inferred
from middleware. Guessing wrong there is worse in both directions:
calling a public endpoint protected hides a hole, and the reverse
invites a bug report.

The contract spec lives in e2e rather than the unit suite because it
needs the whole app mounted, and requiring server.js from node:test
pulls in the database pool and hangs the run — that has happened here
before.

Also: e2e now runs in CI on dev, gated by a shell check inside the step
rather than a job-level "if", which this Forgejo dispatches anyway and
then kills with "Early termination".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 00:52:55 +02:00

78 lines
4.1 KiB
JavaScript

// ============================================================
// OPENAPI — what each endpoint is for
// ============================================================
// Paths and methods are read from the router (src/utils/openapi.js). This is
// the half introspection cannot supply: what an operation does, what it takes,
// and whether it needs a session.
//
// Keyed by "METHOD /path" exactly as the route is mounted, with :params left as
// they appear in the code. A route with no entry here fails test/openapi.test.js
// — that is the point. A new endpoint cannot ship without a sentence saying what
// it is, and an entry for an endpoint that no longer exists fails the same test
// from the other direction.
//
// `public: true` marks an operation that works with no session. Say it
// explicitly: guessing from middleware gets it wrong in the direction that
// hides a hole.
// Path parameters, described once rather than at every route that takes them.
var parameters = {
id: 'Identifier of the record, scoped to the signed-in account.',
workflow: 'Which feature the job belongs to (for example my_resources).',
key: 'Setting key, for example clinical_assistant.chat_model.',
slug: 'URL-safe name of the document.'
};
var operations = {
// ── Session ─────────────────────────────────────────────────────────
'POST /api/auth/login': {
summary: 'Sign in with a password',
description: 'Returns a token and sets the ped_auth cookie. Rate limited.',
public: true
},
'POST /api/auth/login-code/request': {
summary: 'Email a single-use sign-in code',
description: 'Always answers the same way whether or not the address has an account, so it cannot be used to discover who is registered.',
public: true
},
'POST /api/auth/login-code/verify': {
summary: 'Exchange a sign-in code for a session',
public: true
},
'POST /api/auth/register': {
summary: 'Create an account',
description: 'Requires an invitation code while registration is invite-only.',
public: true
},
'GET /api/auth/registration-status': {
summary: 'Whether registration is open, and whether it needs an invitation',
description: 'Read by the sign-in screen to decide whether to offer the register link.',
public: true
},
'POST /api/auth/logout': { summary: 'End this session' },
'GET /api/auth/me': { summary: 'The signed-in account' },
// ── Clinical assistant ──────────────────────────────────────────────
'POST /api/clinical-assistant/ask': {
summary: 'Ask a question against the clinical corpus',
description: 'Streams the answer as server-sent events, with the retrieved sources.'
},
// ── My Resources ────────────────────────────────────────────────────
'GET /api/my-resources': { summary: 'Teaching material belonging to this account' },
'GET /api/my-resources/:id': { summary: 'One saved resource' },
'POST /api/my-resources/generate': { summary: 'Generate a deck or handout' },
'POST /api/my-resources/:id/refine': { summary: 'Revise a saved resource' },
'GET /api/my-resources/:id/export': { summary: 'Download as PowerPoint, Word or PDF' },
'GET /api/my-resources/theme-sample/:id': {
summary: 'A sample deck in one theme',
description: 'Every slide layout with placeholder text, as a PowerPoint file, so a theme can be judged before it is used.'
},
'POST /api/my-resources/:id/to-nextcloud': { summary: 'Send the rendered file to the owner\'s Nextcloud' },
// ── Health ──────────────────────────────────────────────────────────
'GET /api/health': { summary: 'Liveness', public: true },
'GET /api/build': { summary: 'The revision this container is running', public: true }
};
module.exports = { operations: operations, parameters: parameters };