Skip to content

feat: CDS Hooks 2.0.1 service + the OpenAPI integration contract - #469

Merged
Taleef7 merged 7 commits into
mainfrom
feat/cds-hooks-and-openapi
Aug 17, 2026
Merged

feat: CDS Hooks 2.0.1 service + the OpenAPI integration contract#469
Taleef7 merged 7 commits into
mainfrom
feat/cds-hooks-and-openapi

Conversation

@Taleef7

@Taleef7 Taleef7 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Makes the integration surface real and demonstrable. Two stakeholder asks and one tracked TODO converge here, which is why they ship together: a CDS Hooks discovery endpoint is itself an API that wants documenting alongside /api/v1/compliance.

  • Doug asked whether WorkWell has a Swagger API. It did not — probes against production and staging returned 501 not_implemented from all four candidate paths, while ARCHITECTURE.md §9 claimed a springdoc OpenAPI document belonging to a backend retired in [Phase 5] Deploy cutover to Node container + JVM retirement + docs #109 PR4, and §7 did not mention /api/v1/compliance at all.
  • Nicole's concern was reinventing wheels. Shipping the community's standard contract is the direct answer. Adopting its reference implementation at runtime would have replaced a working engine with a second one — ADR-008 stands, cqf-fhir-cr keeps only its B7 cross-check role.
  • Partially delivers proposal P1 (Proposal: encounter-close quality check #458), answering three of its four open questions and sidestepping the fourth (latency) by reading persisted outcomes.

What ships

GET /cds-services Discovery. Public — service metadata, no patient data.
POST /cds-services/{id} Cards for the patient-view hook, from the most recent finalized run. Bearer-gated.
POST /cds-services/{id}/feedback Accepted/overridden per card, audited. Bearer-gated.
GET /api/v1/openapi.json Hand-authored OpenAPI 3.1.1 for the promised surface only. Public.
/api-docs (frontend) Public API reference rendered from that document.

Contracts: docs/CDS_HOOKS.md, docs/COMPLIANCE_API.md. Decisions: ADR-067, ADR-068.

The refusals are the substance

Each is mutation-checked — the test fails when the guard is removed.

  • critical is never emitted, and is unrepresentable in the card type. In CDS Hooks it means the user must not proceed; WorkWell is supplementary to WebChart (locked decision 1) and is not entitled to say that about someone else's encounter. systemActions likewise — nothing we return may change a chart without a human choosing it.
  • An absence is a CARD, not an empty list. A patient with no finalized outcome gets one info card. {"cards":[]} at the point of care reads as "no gaps" — the confusion ADR-061's 404 exists to prevent, and the one that would have hidden the wc|<patientId> namespace trap PROPOSALS_2026-08.md §P1 names. An empty list is reserved for a subject we did evaluate and found clean.
  • No prefetch is declared, because none is evaluated, and usageRequirements says so in the machine-readable contract. Declaring a template we ignore would make a client fetch and transmit for nothing.
  • A suggestion is offered only where the order code carries an APPROVED terminology mapping, read from the store so approving one unlocks it without a code change. Consequence, stated up front: cms122 and cms125 get no suggestion — their CPT codes have no mapping at all, and order-catalog.ts calls its codes "representative (demo, not billing-certified)". Offering a demo-grade CPT for one-click creation in a certified EHR is the harm the rule prevents.

The security-relevant part is small and was nearly invisible

/cds-services is outside /api/, and authorize ends in return { ok: true } for non-/api paths — permitAll, mirroring Spring's anyRequest().permitAll(). Without an explicit rule the invoke endpoint would have served per-patient clinical status to anonymous callers. Two rules added, ordering load-bearing (/cds-services/** also matches the bare path), asserted both as a unit authorize call and end-to-end through the worker. Deleting either rule fails both assertions.

Both guards caught real things, and neither would have caught the other's

The OpenAPI document is hand-authored, so the contract test is treated as the other half of the decision rather than garnish. It asserts two-way coverage: every documented (path, method, status) produced by a real request through the real worker, and every observed response documented. A documented-but-unrouted path fails with documented but NOT ROUTED — which is exactly how ARCHITECTURE.md came to assert an OpenAPI document for a year after the JVM was retired.

  • The coverage test refused a mis-modelling of mine. I had documented a 405 under the GET operations to silence a Redocly warning; 405 belongs to the path, so the document would have asserted that a GET returns 405. The test failed and the 405s came out.
  • Redocly rejected five uses of nullable — a 3.0 keyword OpenAPI 3.1 removed in favour of type unions. My hand-rolled check structurally cannot see that class. Same lesson as the CVU+ XSD/Schematron episode: a check's scope was narrower than the claim it was cited for.

Five warnings remain, explained in spec.ts rather than ignore-filed.

Research corrected three assumptions before code was written

  • The auth model is not RFC 7523. CDS Hooks defines a bespoke signed-JWT bearer scheme (RFC 7515/7517/7518) with no sub, no token endpoint, and it SHALL NOT be signed with a symmetric algorithm — so our HS256 token can never be a conformant CDS Hooks JWT. Recorded as a named gap, not built: jku fetching is SSRF-by-design, and a verifier whose allowlist nobody has populated is a control that reads as present and cannot fire.
  • No graded conformance ruler exists. The community validator is JSON Schemas last pushed 2018-02-05, the sandbox emits no score, and Inferno has no CDS Hooks kit. So STANDARDS_CONFORMANCE.md says structurally conformant, self-graded, unverified by any external suite — the ADR-065 posture.
  • The outcome-to-card mapping is ours. HL7 blesses PlanDefinition/$apply then RequestOrchestration then cards, but DEQM's $care-gaps stops at a DetectedIssue and nobody publishes the gap-to-card bridge. Recorded as a local mapping rather than cited to an IG.

No new dependencies

Zero, runtime or dev. Redocly runs via pinned npx with no package.json entry. The reference page is hand-rolled — and not only on the rule: swagger-ui-react peers on react@">=16.8 <19" and this app is React 19, Swagger UI's dark mode is a hard-coded html.dark-mode class that would contest ownership of <html>, and Scalar and Redoc both default to a CDN script the CSP and offline demo rule out. Trade: no try-it-out console, a copyable curl instead.

Other

  • No schema change. card.uuid derives from (runId, subjectId, measureId), so feedback correlates by recomputation. Schema is the owner's alone (CLAUDE.md).
  • CORS is not relaxed — a browser-based CDS client's origin must be added to WORKWELL_CORS_ALLOWED_ORIGINS deliberately; the spec requires CORS support but declines to specify an allowlist rule.
  • Two ARCHITECTURE corrections, including one I didn't go looking for: §9 promised /api/version returns uptime; it does not.
  • S7 rewrittendelivering a finding, offering an order and did anyone act move to built; evaluating data supplied on the request stays not built, which is step 2, and prefetch is where it would go.
  • docs/transcripts/ is now gitignored — only basename globs matched before, so a file like docs/transcripts/2026-08-16 call.md was committable. Verified with git check-ignore.
  • First smoke coverage of the integration surface — none of it was probed before.

Verification

backend-ts:  tsc clean · 1970 tests, 0 fail, 15 self-skips
redocly:     0 errors, 5 warnings (exit 0)
frontend:    lint clean · 186 tests · build clean · /api-docs prerendered

Also driven end-to-end through the real worker: discovery public; invoke 401 without a token; the audiogram card carrying a ServiceRequest at intent=proposal / status=draft with CPT 92557; cms125 carrying a link and no suggestion; an unknown patient getting one info card; feedback returning a bodyless 200.

Not verified: the HTTP-level run. @mieweb/cloud-local wants Redis and a service on :9000, neither running locally — a pre-existing environment requirement, not introduced here.

Owner steps this creates

Two, both cheap and both for other people.

  1. MIE's CHPL entry could not be read (SPA + key-gated API), so whether WebChart holds §170.315(b)(11) is a manual lookup. Note (b)(11) and HTI-1 do not name CDS Hooks — nothing here is justified by certification.
  2. The joint call now has a precise question: does WebChart act as a CDS Hooks client, and if so what are its iss and JWKS URL? No public evidence either way — zero hits across both MIE docs sites.

Not done, deliberately

No encounter-start service (maturity 1 in the CDS Hooks Library IG, and it would return the same cards); no prefetch evaluation; no OpenAPI path aliases beyond the canonical one; no overrideReasons; no tenant scoping (#269, pre-existing).

🤖 Generated with Claude Code

Taleef and others added 4 commits August 17, 2026 11:35
…outcomes

Adds GET /cds-services (public), POST /cds-services/{id} and its /feedback
sibling (both bearer-gated), so an open measure gap can reach a clinician
through the community standard rather than a bespoke endpoint.

Cards render a COMPLETED run's outcome; they never trigger an evaluation. No
prefetch is declared, because none is evaluated -- usageRequirements says so in
the machine-readable contract. Three refusals are load-bearing and
mutation-checked: critical is unrepresentable in CdsCard, since WorkWell is
supplementary to WebChart and may not tell a clinician not to proceed; a
suggestion is offered only where the order code has an APPROVED terminology
mapping read from the store, so cms122/cms125 carry information and a link
rather than a demo-grade CPT for one-click creation; and a patient with no
finalized outcome gets an informational card, never an empty card list, which
at the point of care would read as "no gaps".

Card uuids derive from (runId, subjectId, measureId), so feedback correlates by
recomputation and the endpoint needs no schema change. The measure-outcome to
card mapping is ours: HL7 blesses PlanDefinition/$apply -> RequestOrchestration
-> cards, and the DEQM care-gap to card leg is unpublished.

/cds-services is outside /api/, where authorize ends in permitAll, so the two
new rules are mandatory rather than a refinement -- asserted both as a unit call
and end-to-end through the worker, and both fail when either is removed.

Suite 1964, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed against drift

GET /api/v1/openapi.json now serves a hand-authored 3.1.1 document covering the
PROMISED surface only -- /api/v1/compliance, the three /cds-services operations,
health and version -- and stating that the ~40 internal /api/** routes are
excluded because they carry no stability promise. Public: reading a contract
should not require credentials.

The guard is what makes hand-authoring defensible, and it is two-way. Every
(path, method, status) the document declares is produced by a real request
through the real worker, and every response the tests observe is declared. A
documented route that is not routed fails with "documented but NOT ROUTED" --
which is precisely how ARCHITECTURE.md came to assert a springdoc OpenAPI
document for a year after the JVM was retired. All three directions
mutation-checked: deleting the route, removing a produced status, and requiring
an absent property each fail the intended assertion.

Redocly lints the document in CI (pinned, telemetry off, no ignore file) because
it catches a class the contract test cannot -- and did on the first run: five
uses of `nullable`, which OpenAPI 3.1 removed in favour of type unions. The five
remaining warnings are explained in spec.ts rather than silenced; four are
operations that genuinely have no 4xx, and their 405 belongs to the path, not the
GET, which the coverage test correctly refused to let us mis-model.

Also adds the first smoke coverage of the integration surface: the document, CDS
discovery, one real card assembly and one v1 compliance read. None was probed
before.

Suite 1969 across the affected files, 0 fail. Redocly: 0 errors, 5 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ment

/api-docs is a top-level route, deliberately outside (dashboard) and
unauthenticated: the document it renders is public, and an integrator should be
able to read the contract before they have credentials. It fetches with a plain
fetch rather than lib/api/client, whose token attachment and silent-refresh
behaviour would be wrong on a page nobody is logged into.

Hand-rolled rather than vendored, and not only on the no-new-dependency rule:
swagger-ui-react peers on react ">=16.8 <19" and this app is on React 19, so the
React integration does not exist for us; Swagger UI's dark mode is a hard-coded
html.dark-mode class that would fight this app for ownership of <html>; and
Scalar and Redoc both default to a CDN script that the CSP and offline demo rule
out. The page inherits the Enterprise Health brand and dark mode instead
(ADR-004). The trade is no try-it-out console -- a copyable curl instead, which
for a bearer-token API is about as useful and needs no proxy.

The load-bearing test is that no operation is silently dropped: an untagged
operation lands under "Other" rather than vanishing, which is the same defect
class as a documented-but-unrouted path one layer up. A 3.1 type union renders as
"string | null" rather than "string", so a nullable field reads as nullable.

Frontend: lint clean, 186 tests pass, build clean, /api-docs prerendered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… stale ARCHITECTURE claims

ADR-067 (CDS Hooks cards render a completed evaluation and never trigger one)
and ADR-068 (the OpenAPI document covers the promised surface only, guarded by a
routed-path test), plus docs/CDS_HOOKS.md as the contract an integrator reads.

Two ARCHITECTURE corrections, one of which was the point of the work: §9
asserted "The OpenAPI document (workwell.swagger.enabled=true) advertises
version v1" -- a springdoc property from a backend retired in #109 PR4 -- while
§7 did not mention /api/v1/compliance at all, so the file simultaneously claimed
a document it did not serve and omitted the one contract it does. Writing the
document also surfaced that §9 promised /api/version returns `uptime`; it does
not.

STANDARDS_CONFORMANCE gains two rows written through the conformance skill. Both
name who graded what: CDS Hooks 2.0.1 is structurally conformant and SELF-graded,
because no external grader exists (the community validator is JSON Schemas last
pushed 2018, the sandbox is ungraded, Inferno has no CDS Hooks kit); and the
gap-to-card leg is recorded as a LOCAL mapping, since HL7 blesses
PlanDefinition/$apply -> RequestOrchestration -> cards but nobody publishes a
route from a DEQM care gap to a card. Neither row is justified by certification:
ONC (b)(11) and HTI-1 do not name CDS Hooks.

AI_GUARDRAILS gains §1.1 -- a card is a rendering, every clinical statement in it
is the CQL outcome verbatim, and systemActions is never emitted.

S7 is rewritten with its per-part table split along the line that matters:
delivering a finding, offering an order, and did-anyone-act move to built, while
evaluating data supplied on the request stays not built -- that is step 2, and
prefetch is where it would go. The sequence diagram draws that leg dashed.

Also closes a real gitignore gap: docs/transcripts/ was only covered by basename
globs, so `docs/transcripts/2026-08-16 call.md` was committable. Verified with
git check-ignore.

Backend suite 1970, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const bytes = new TextEncoder().encode(parts.join(""));

P2 Badge Replace the literal NUL byte with an escaped separator

This string contains an actual NUL byte rather than an escaped source representation. Git consequently classifies the entire TypeScript file as binary (git diff reports Binary files differ), hiding this core card-building implementation and all future edits from normal review and text-based tooling. Preserve the same hash input with an escaped form such as "\0" or "\u0000" so the file remains ordinary text.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend-ts/src/routes/cds-hooks.ts Outdated
Comment on lines +108 to +110
} catch (err) {
// Best-effort at the response boundary: an audit failure must not turn a correct answer into a 500.
console.error(`WORKWELL_ALERT cds-hooks audit write failed: ${String(err)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail feedback requests when the audit write fails

When appendAudit fails during a feedback POST, this catch suppresses the failure and the handler still returns 200, even though the audit event is the only persisted record of the accepted/overridden action. A transient database failure therefore permanently loses the feedback and tells the CDS client not to retry; feedback writes must propagate the failure (even if invocation/read auditing remains best-effort) to preserve the repository's no-exceptions audit invariant.

AGENTS.md reference: AGENTS.md:L5-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and fixed in 3af2d83 — the most serious finding, and my own reviewer raised it independently.

Feedback now returns 503 audit_write_failed rather than 200, with recorded / of so a retry is informed, and the body states retry is safe (feedback is idempotent by (card, outcome, outcomeTimestamp)). Invoke deliberately stays best-effort, and auditCds now returns a boolean so the two call sites make that difference explicit instead of sharing one swallow.

You identified exactly why the asymmetry matters: for invoke the cards are still correct, so a 500 would be the worse outcome; for feedback the audit event is the persistence (ADR-067 d10 — the whole reason the endpoint needed no schema change), so a swallowed failure was a silent no-op that told the client never to retry. Recorded as ADR-067 decision 13, pinned by a test that injects a throwing store.

Comment on lines +244 to +245
: await buildComplianceCards(found.rows, {
subjectId: found.subjectId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the hook patient ID in suggested FHIR resources

When a WebChart request supplies a bare patient ID such as 4821, lookup intentionally resolves it to the internal key wc|4821, and this line passes that internal key into order construction. toServiceRequest() consequently emits subject.reference: "Patient/wc|4821"; | is not valid in a FHIR resource id and the reference also does not identify the client's Patient/4821, so approved-order suggestions cannot be applied by a conforming CDS client. Keep the internal subject ID for outcome lookup/card identity, but use the original hook patientId for the suggested FHIR resource.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and fixed in 3af2d83. This is the one that only bites on the deployment the feature exists for, which is what makes it a good catch.

buildComplianceCards now takes the hook patientId alongside the internal subjectId, and the emitted resource is re-pointed via a small withClientSubject helper: Patient/4821, not Patient/wc|4821. Kept out of toServiceRequest itself deliberately — GET /api/orders/proposals shares that function and the internal id is the right answer there, so changing it at the source would have fixed one caller by breaking another.

Card and suggestion identity still derive from the internal subject id, so feedback correlation is unaffected. Pinned by a test that builds with subjectId: "wc|4821" / patientId: "4821" and asserts both the reference and that no pipe survives into a FHIR id. CardOptions.patientId is required rather than optional, so the compiler forces every future call site to state which id it means.

properties: {
card: str("The `card.uuid` from the invoke response."),
outcome: { type: "string", enum: ["accepted", "overridden"] },
acceptedSuggestions: { type: "array", description: "Required when `outcome` is `accepted`.", items: { type: "object", properties: { id: str("The `suggestion.uuid`.") } } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Model accepted suggestion identifiers as UUID strings

CDS Hooks 2.0.1 defines acceptedSuggestions as an array of suggestion UUID strings, but this schema advertises objects containing an id. Generated clients and the new reference page will therefore produce values such as [{"id":"..."}] (or even [{}]) instead of ["..."], making the published integration contract nonconformant despite the runtime's permissive array-only check. Change the OpenAPI schema, TypeScript wire type, documentation, and tests to use string items.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this against the specification and I do not think it is right, so I have left the schema as it is — flagging rather than silently declining.

CDS Hooks 2.0.1 defines acceptedSuggestions as an array of json objects, not of strings. From the Feedback section:

acceptedSuggestions | CONDITIONAL | array | An array of json objects identifying one or more of the user’s AcceptedSuggestions. Required for accepted outcomes.

and each AcceptedSuggestion is:

id | REQUIRED | string | The card.suggestion.uuid from the CDS Hooks response.

Every example in the spec is the object form, e.g.

"acceptedSuggestions": [ { "id": "e56e1945-20b3-4393-8503-a1a20fd73152" } ]

So the published schema (array of objects with a required id) matches the contract, and changing it to string items would make our document non-conformant rather than fix it — which is the direction that would actually hurt, since the whole point of publishing it is that a generated client can be trusted.

Two things your comment did surface that were real, and are fixed in 3af2d83: the runtime check was permissive (it required the array but not the ids), and the audit event did not record which suggestions were accepted. Both now do. Happy to reopen if you are reading a different revision of the spec — a pointer to it would settle it.

@Taleef7 Taleef7 self-assigned this Aug 17, 2026
Taleef and others added 2 commits August 17, 2026 13:33
…and an engine failure is not a clinical claim

Follow-up to the code review and Codex's PR comments. Seven substantive fixes,
four vacuous guards replaced, and the doc claims corrected where they overstated.

Correctness and safety:

- Feedback returns 503, not 200, when its audit write fails. The audit event IS
  the entire persistence for that endpoint, and the spec gives feedback no
  response body to signal partial success, so a swallowed failure told the client
  never to retry and lost the accepted order silently. Invoke stays best-effort,
  since its cards are correct regardless. Both reviewers raised this
  independently; CLAUDE.md's "every state change writes audit_event -- no
  exceptions" decides it.
- A FAILED evaluation is reported as ours. PARTIAL_FAILURE rows are served, and a
  subject whose evaluation threw persists as MISSING_DATA + evaluationError --
  which deriveCell rendered as "No record on file" and nextActionFor turned into
  "Collect the missing documentation", asserting a fact about the patient when our
  engine threw. Now a "could not be evaluated" card, info, no suggested order.
- A suggested ServiceRequest references the id the CLIENT sent. On a live tenant
  it carried Patient/wc|4821, which names nothing the client can resolve and is
  not a legal FHIR id, so the suggestion could not be applied (Codex). Re-pointed
  in the CDS layer only; /api/orders/proposals is unchanged.
- Feedback is bounded: 100 entries per request, userComment truncated at 8000
  chars. Each entry is an append to the append-only ledger by a machine
  credential, and userComment is the first unstructured clinical prose in
  audit_events -- now noted in the PHI posture.
- card.uuid is a conformant UUID (version 8 per RFC 9562, variant 10xx). CDS
  Hooks types it as a UUID and a validating client would reject a bare hash.
- refRunId was dead: no caller ever set detail.runId. The invoke event now records
  the run and maps each emitted uuid to its measure, which makes the documented
  correlation claim true -- it previously overstated by a step, since inverting a
  uuid meant a search over every run in the subject's history.
- Removed the authorize rule for /api/v1/openapi.json: handleOpenApi runs before
  the auth gate, so the rule could never be consulted while reading as the control.

Guards that could not fail, now replaced:

- The alias assertion was `assert.notEqual(authorize(...).ok && alias === PATH,
  true)` -- the && is always false, so it passed for any implementation. Now
  asserts the aliases return the worker's 501 not_implemented, with a token, so it
  proves "not served" rather than "not reachable".
- The `critical` loop asserted a value against a type that permits only those two
  values. Replaced with the priorityFor mapping, which is failable.
- assert.ok(Array.isArray(cardUuids)) pinned a field name and nothing else, and
  `.find()` returned the OLDEST event despite being named `latest`. Now
  deep-equals the uuids the client received.
- The openapi fixture had no APPROVED mapping, so the suggestion subschemas -- the
  deepest in the document -- were never schema-validated. Added.

Also: the reachability walk starts from the paths rather than every component (a
dead schema referencing another marked its target live) and follows $ref into its
target; parameter schemas are walked; endpointsOf skips non-operation path-item
keys; and a test pins dispositionFor's OPEN set equal to order-proposal's AT_RISK
set, an undocumented coupling the suggestion dedupe silently depends on.

One finding NOT acted on: Codex's claim that acceptedSuggestions is an array of
UUID strings. The spec defines it as "an array of json objects identifying one or
more of the user's AcceptedSuggestions", each with a REQUIRED `id` string, and
every example is [{"id": "..."}]. The schema was already right.

And a self-inflicted one worth recording: a sed -i over this UTF-8 source replaced
a space with a literal NUL byte, twice, and it reached a commit -- it typechecked,
every test passed, and the only symptom was grep reporting the file as binary.
Fixed, with a test that would have caught both.

backend 1976 tests / 0 fail; frontend 187 / lint clean / build clean; redocly 0
errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The feedback check required `acceptedSuggestions` to be present but not that its
entries identify anything, so `[{}]` passed and would have been recorded as an
accepted suggestion nobody can resolve. The spec makes AcceptedSuggestion.id
REQUIRED -- it is the card.suggestion.uuid we emitted -- so this is now enforced,
with `[{}]` and `[{"id":""}]` pinned as 400s.

Surfaced while replying to a Codex comment that argued acceptedSuggestions should
be an array of strings rather than objects. That part is not right -- the spec
defines "an array of json objects identifying one or more of the user's
AcceptedSuggestions", each with a required `id`, and every example is the object
form -- but checking it properly exposed that our runtime was more permissive than
the schema we publish.

Suite 1976, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w round

CDS_HOOKS.md "Limits, stated" now says an invocation is a bounded but unindexed
read rather than a constant-time one, and points at #470 -- an integrator reading
the contract should not have to discover that. Names the four other callers that
share the scan, so it reads as a pre-existing property rather than a property of
this endpoint.

JOURNAL records what the review changed: the two findings both reviewers reached
independently, the four guards of mine that could not fail, the one Codex comment
I declined with the spec text, and the NUL byte a sed run put in source twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Taleef7
Taleef7 merged commit 5c3c549 into main Aug 17, 2026
19 checks passed
@Taleef7
Taleef7 deleted the feat/cds-hooks-and-openapi branch August 17, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant