Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ jobs:
pnpm compile-measures
git add -A -- src/engine/cql/elm src/measure/resources/cql-resources.json
git diff --cached --exit-code --stat -- src/engine/cql/elm src/measure/resources/cql-resources.json
# The OpenAPI document must be valid OpenAPI, which `openapi.test.ts` deliberately does NOT check: its
# job is agreement between the document and the running worker, and no amount of that catches a 3.0-ism
# like `nullable` (which 3.1 removed, and which this found on the first run). Two different guards.
# Pinned exactly — `@latest` would make the gate non-reproducible, the same reason the terminology fetch
# is pinned. REDOCLY_TELEMETRY=off is not optional: the CLI otherwise reports environment-variable
# values and the names of the rules that fired. Exits 0 on warnings, non-zero on errors; the five
# expected warnings are explained in src/openapi/spec.ts and are deliberately not ignore-filed.
- name: The OpenAPI document is valid OpenAPI 3.1
env:
REDOCLY_TELEMETRY: "off"
run: |
node --import tsx scripts/openapi-emit.mjs "$RUNNER_TEMP/openapi.json"
npx --yes @redocly/cli@2.46.1 lint "$RUNNER_TEMP/openapi.json"
- name: Test (SQLite floor + Postgres ceiling)
env:
WORKWELL_TEST_PG_URL: postgres://workwell:workwell@localhost:5432/workwell
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ docs/vision doc screenshots
webchart-import/

# Local-only call transcripts (personal/personnel content - never commit, never reference in docs).
# The directory AND the filename globs, because the globs below match on the BASENAME: a file named
# `docs/transcripts/2026-08-16 call.md` matches none of them and was committable by accident.
docs/transcripts/
# Any depth, any of the formats a recorder/transcriber emits.
**/*transcript*.txt
**/*transcript*.md
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ to this list without deleting from it — the whole point is that it stays small
backend retired in #109 PR4, and its stop condition died with the JVM. Now in `docs/archive/`.)
- @docs/AI_GUARDRAILS.md — the "AI never decides compliance" hard rule lives or dies on this
- @docs/DATA_MODEL_CONTRACTS.md — idempotency + `evidence_json` + CSV contracts; Definition of Done makes these mandatory on EVERY PR
- @docs/ADR_INDEX.md — 65 ADR titles only, so a session knows a decision exists; bodies stay in DECISIONS.md
- @docs/ADR_INDEX.md — 67 ADR titles only, so a session knows a decision exists; bodies stay in DECISIONS.md
- @docs/LOCKED_DECISIONS.md — owner-locked decisions (§4, rewritten 2026-08-04 per ADR-058) + the dated 2026-07-24 audit facts (§5)

## Other docs to consult on demand
Expand Down
1 change: 1 addition & 0 deletions backend-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"test": "node --import tsx --test \"src/**/*.test.ts\" \"packages/*/src/**/*.test.ts\" \"scripts/**/*.test.mjs\" \"scripts/**/*.test.ts\"",
"build:packages": "node scripts/build-packages.mjs",
"verify:publish": "node scripts/verify-publish.mjs",
"openapi:emit": "node --import tsx scripts/openapi-emit.mjs",
"official:terminology-audit": "node scripts/official-terminology-audit.mjs",
"vendor:official": "node scripts/vendor-official-measure.mjs"
},
Expand Down
24 changes: 24 additions & 0 deletions backend-ts/scripts/openapi-emit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Emit the OpenAPI document to a file, so an external validator can read it (ADR-068).
*
* node --import tsx scripts/openapi-emit.mjs [outfile] # default: openapi.json in the cwd
*
* Why this exists rather than a committed `openapi.json`: the document is generated from
* `src/openapi/spec.ts`, so a committed copy would be a second artifact that can disagree with the served
* one — the exact drift the contract test exists to prevent. CI emits it to a temp path, lints it, and
* throws it away; the only source of truth is the code the worker serves.
*
* CI (`.github/workflows/ci.yml`, the backend-ts job):
* node --import tsx scripts/openapi-emit.mjs "$RUNNER_TEMP/openapi.json"
* REDOCLY_TELEMETRY=off npx --yes @redocly/cli@2.46.1 lint "$RUNNER_TEMP/openapi.json"
*
* Redocly is pinned exactly, for the same reason the official-terminology fetch is: `@latest` in CI makes
* the gate non-reproducible. `REDOCLY_TELEMETRY=off` is not optional — the CLI otherwise reports
* environment-variable values and the names of rules that fired.
*/
import { writeFileSync } from "node:fs";
import { openApiDocument } from "../src/openapi/spec.ts";

const out = process.argv[2] ?? "openapi.json";
writeFileSync(out, `${JSON.stringify(openApiDocument(), null, 2)}\n`);
console.log(`wrote ${out}`);
20 changes: 20 additions & 0 deletions backend-ts/src/auth/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,30 @@ const RULES: Rule[] = [
{ pattern: rx("/api/health"), access: "PERMIT" },
{ pattern: rx("/api/version"), access: "PERMIT" },
{ pattern: rx("/health"), access: "PERMIT" },
// NOTE: `/api/v1/openapi.json` deliberately has NO rule. Like health and version, `handleOpenApi` runs in
// the worker BEFORE the auth gate, so a rule here would never be consulted — a control that reads as
// load-bearing and cannot fire (review). Its public-ness is asserted by an actual unauthenticated request
// in `openapi.test.ts`, which is the only thing that proves it.

{ pattern: rx("/sse"), access: [A, CM, MCP] },
{ pattern: rx("/mcp/**"), access: [A, CM, MCP] },

// CDS Hooks (ADR-067). These rules are MANDATORY, not a refinement: `/cds-services` matches no `/api/**`
// rule, and `authorize` ends with `return { ok: true }` for non-`/api` paths (permitAll, mirroring
// Spring's anyRequest().permitAll()) — so without them the invoke endpoint would serve per-patient
// clinical status to anonymous callers.
//
// Discovery is PERMIT: it returns service metadata and no patient data, and a CDS client must be able to
// discover the service during onboarding (the spec imposes no auth on it). Invoke and feedback are
// machine-client work, so they reuse the SAME authority as /sse and /mcp/** rather than inventing a role
// — the user directory stays hardcoded (CLAUDE.md hard rule).
//
// Order is load-bearing: `rx("/cds-services/**")` expands to `^/cds-services(?:/.*)?$`, which ALSO
// matches the bare path, so the GET-exact PERMIT must come first. A non-GET on `/cds-services` therefore
// falls to the gated rule and is authenticated before the handler answers 405.
{ method: "GET", pattern: rx("/cds-services"), access: "PERMIT" },
{ pattern: rx("/cds-services/**"), access: [A, CM, MCP] },

// Outreach templates: the picker on the case-detail outreach action is the CASE_MANAGER's
// primary consumer, so READING the template list/preview is CM/ADMIN (Fable M23). Writes
// (create/update/delete) stay ADMIN via the /api/admin/** rule below. First-match-wins, so
Expand Down
Loading
Loading