# IQ Apparatus render contract — changelog

The contract is versioned by revision date. Every API response carries
`contract_version` in the envelope, and course payloads repeat it inside `data`
so it survives consumers that unwrap the envelope.

Canonical module (zero imports, safe to vendor):

- Latest: `https://iqapparatus.com/contract/iq-render-contract.ts`
  (plain-text mirror: `.../iq-render-contract.ts.txt`)
- Pinned: `https://iqapparatus.com/contract/iq-render-contract-2026-09-03.ts`
  (plain-text mirror: `.../iq-render-contract-2026-09-03.ts.txt`)
- JSON Schema: `https://iqapparatus.com/contract/iq-render-contract.schema.json`
- Fixtures: `https://iqapparatus.com/contract/fixtures/index.json`
- Rendering spec: `https://iqapparatus.com/bramwell-rendering-spec.md`
- Theme: `https://iqapparatus.com/bramwell-theme.css`

## Compatibility policy

All changes are additive. Fields are never removed or retyped within a
revision. Consumers must ignore unknown fields, render unknown component and
phase types as a notice rather than dropping them, and treat an unrecognized
`contract_version` as "newer than me, render what I know".

---

## 2026-09-03

Declares within-phase pagination, so a consumer never has to guess where a
phase breaks into screens. Purely additive.

Per component (on every delivered component):

- `screen_index` — 0-based screen within the phase.
- `screen_role` — `primary` starts a screen and is what Continue advances
  from; `attached` renders on the preceding primary's screen.
- `anchor_id` — stable within-phase anchor for deep links and scrolling.

Per phase:

- `display_mode` — `sequential` (one screen at a time with Continue, the IQ
  default) or `stacked` (the phase scrolls as one screen).
- `screen_count` — number of distinct screens in the phase.

Guarantees:

- A "Meet the Patient" `introduction` phase is ONE screen: the scene image,
  hook prose and confidence slider all carry `screen_index: 0`.
- A content/HTML block with no required interaction is its own `primary`
  screen, so Continue always has a defined next target. This is the fix for
  content-only screens where Continue appeared enabled but did nothing.
- A `choice` / `form-input` authored directly beneath a text block is
  `attached` to that block's screen, matching the IQ player.

`reveal_after` / `reveals_on_answer` are unchanged and remain pure visibility
gates — never screen breaks. Do not derive pagination from them.

Fixtures `bramwell-series-module.json` (now showing a 5-screen content phase),
`media-pending.json`, `catalog-list.json` and `whoami.json` were re-emitted at
`2026-09-03`; `unknown-future-revision.json` is unchanged.

---

## 2026-09-02

Closes an answer-key leak in the delivery payload. `debrief.answerKeyHtml`
states the correct answer for every decision, so it now follows the same
`outcomes:read` gate as `follow_up.recheck[].options[].is_correct`.

- Keys **with** `outcomes:read`: unchanged — `answerKeyHtml` is delivered.
- Keys **without** it: `debrief.answerKeyHtml` is `null`; `caseTakeaway` and
  `nextStepsHtml` are unchanged. Correctness comes from
  `POST /api-submit-progress`, as it always has.
- New field `debrief.answer_key_available` (boolean) tells a consumer an answer
  key exists for the activity even when this key may not receive it, so the
  results screen can defer to the platform instead of rendering an empty card.

The Oncology Data Advisor key holds `outcomes:read`, so its payloads are
byte-identical to `2026-08-31` apart from the added boolean.

---

## 2026-08-31

Declares the Bramwell hook scene illustration. Consumers previously received a
"Meet the patient" phase containing a single `html`/`rich-text` component while
IQ Apparatus' own player showed a scene image above it — the image was a
course-level authoring field the payload never mentioned, so it could only be
inferred.

Added:

- The scene illustration now ships as a real `image` component, first in the
  `components` array of the first `introduction` phase:
  `{ id, type: "image", url, alt, caption, source: "bramwell_patient_image",
  alt_is_fallback }`. Render it like any other image component; do not
  special-case it. It is omitted when the activity has no scene illustration.
- It is never injected when the phase already carries an authored inline
  `image` component, so a page can never show two scenes.
- `IMAGE_POLICY` in the contract module: every delivered image URL is absolute
  and publicly fetchable without authentication (this now also holds for
  catalog `card_display_config.image`, which is absolutized on the way out).
- `alt_is_fallback: true` marks alt text IQ supplied because the activity
  carried none; a consumer may substitute its own.

Nothing was removed or retyped. A client pinned to `2026-08-28` keeps working
and simply does not know the component is guaranteed.

---

## Documentation only — 2026-08-24 (no version bump)

No payload change. `contract_version` stays at `2026-08-31`; vendored clients do
not need to re-vendor.

- Rendering spec gains **§12 Navigation microcopy**, documenting the button
  wording IQ Apparatus' own player uses (`Back`, `Next` within a phase,
  `Continue` between phases, `Submit answer` / `Submit answers` /
  `Submit assessment` for graded commits, `Saving…` / `Submitting…` for pending
  states, sentence case throughout). These strings are host-owned and are
  deliberately absent from the payload — there is no `next_label` or
  `submit_label` field.
- Catalog example markup updated to sentence case (`Start learning`).
- Conformance checklist gains a navigation-label item.

---

## 2026-08-28


Makes decision gating explicit. Consumers previously had to infer, from prose,
that the blocks authored after a decision must stay hidden until the learner
answers — the outcome video and teach-back debrief state the correct answer, so
rendering them up-front spoils the decision.

Added:

- `reveal_after: string | null` and `reveals_on_answer?: boolean` on **every**
  component in `GET /api-course-content`. `reveals_on_answer: true` marks a gate
  anchor (a `choice`, `question`, or `timed-quiz`); every component after it in
  the same phase carries `reveal_after: <anchor id>`. Anchors are never gated by
  an earlier anchor. Components that render immediately have
  `reveal_after: null`.
- `id` is now guaranteed on every delivered component, synthesized as
  `<phase id>-c<index>` when the authoring record lacked one, so a gate is
  always addressable.
- `ComponentGating` and `DeliveredComponent` types, and the `GATING_POLICY` and
  `CONFIDENCE_CHECK_CONTROL` constants in the contract module and JSON Schema.

Clarified (no payload change):

- Gating is a **visibility** rule, not a reorder. Render `components` in the
  order returned and reveal gated blocks in place.
- `confidence-check` is always a slider (`statement`, `min`, `max`, `minLabel`,
  `maxLabel`). It never carries an `options` array; a payload that does is
  malformed. Documented degraded mode remains a radio scale over the same
  integer values.
- Rationale sourcing is unchanged: local reveal for `client_graded: true`
  decisions, `POST /api-submit-progress` for `assessmentMode` items.

Spec: new section 4a in `bramwell-rendering-spec.md`, plus two conformance
checklist items.

---

## 2026-08-27

Fixes the choice-component grading parity gap: consumers were waiting on a
server verdict for embedded clinical decisions that the grader never produced.

Added:

- `client_graded` on every `choice` component in `GET /api-course-content`, plus
  the `ChoiceComponentDelivery` type. `assessmentMode: true` stays scored and
  fully stripped (key, component rationale, and now option-level `isCorrect` /
  `rationale` too). A non-assessment `choice` with an authored key now ships
  that key contractually: `correctAnswerIndex` normalized to a **0-based index**
  into `options`, plus `rationale` — reveal locally, exactly as IQ's own learner
  UI does. Poll-style choices report `client_graded: false` with no key.
- `POST /api-submit-progress` now grades `choice` components (keyed by component
  id) in addition to `mcq` / `quiz` / `assessment` question banks. Keys are
  resolved from any authoring shape: `options[].isCorrect`, `correctAnswerIndex`
  / `correctIndex` (0- or 1-based), and literal value / label / letter keys.
- Every submitted answer now yields a `question_results` row carrying `graded`
  (`QuestionResult` type). Ungraded rows carry `graded: false`, null
  `correct_answer` / `is_correct`, and `ungraded_reason` of
  `no_authored_answer_key` or `question_id_not_found_in_phase`. Never block a
  reveal waiting for a verdict on an ungraded row.
- `passing_score` is now returned on **every** submission response, including
  ungraded and formative ones, resolved from activity config then phase config,
  defaulting to 70.

Changed:

- Injected pre/post/evaluation phases now use the authored titles
  (`pretestTitle`, `posttestTitle`, `evaluationTitle`, at config root or in
  `templateConfig`). `Pre-Assessment` / `Post-Assessment` / `Course Evaluation`
  are defaults only, no longer hardcoded over an author's label.
- Embedded decisions inside content phases are formative: they return graded
  results but never produce a phase `score` or gate completion.

Nothing removed or retyped; consumers pinned to earlier revisions keep working.

---

## 2026-08-26

No payload change. Rendering-fidelity artifacts: a consumer can now measure its
own render against ours without access to a running IQ Apparatus instance.

- New public reference render at `https://iqapparatus.com/reference/bramwell`.
  Unauthenticated, no database reads, driven by a frozen fixture through the
  real Bramwell primitives. Every canonical surface carries a stable
  `data-ref` attribute. Query controls: `?surface=<id>` (single surface),
  `?state=resting|selected|correct|incorrect`, `?w=<px>` (fixed content
  width), `?chrome=0` (strip page chrome for pixel capture).
- New geometry snapshot at
  `https://iqapparatus.com/contract/bramwell-geometry.json` (plain-text mirror
  `.json.txt`). Computed width, height, padding, border radius, border width,
  gap, font family/size/weight, line height, color, and background for every
  `data-ref` surface at 375 / 768 / 1280 in each of the four states. Captured
  in headless Chromium at device scale factor 1.
- Rendering spec gains section 11, "Rendered geometry", with the capture and
  diff recipe and the tolerance rule.

---

## 2026-08-25

Additive. Multi-key routing: a consumer holding several provider keys can now
tell *why* a fetch missed and which key to retry with.

- New `error_code` on 4xx envelopes from `GET /api-course-content`,
  `GET /api-completions`, and `POST /api-submit-progress`. Values:
  `course_not_found`, `course_not_published`, `course_not_owned_by_key`,
  `invalid_api_key`, `insufficient_scope`, `rate_limited`. Route on this field,
  not on the prose in `error`.
- New `owning_organization_id` on the envelope when `error_code` is
  `course_not_owned_by_key`: the organization that actually owns the requested
  course. Retry with the key bound to that organization.
- New `key_name` on `GET /api-whoami`. Diagnostics only — labels are operator
  free text and may not match the bound tenant. Always route on
  `organization_id`.
- `last_used` on a key now updates on every authenticated call, and request
  logging no longer drops rows when `x-forwarded-for` carries an address chain.
  No response-shape change; it makes per-key traffic visible in the portal.

---

## 2026-08-24


Additive. Landing-page performance: no second round-trip for the overview.

- New `overview` object on every `GET /api-courses` catalog item (list and
  single). It carries the fields a course landing page needs before
  enrollment: `about`, `statement_of_need`, `learning_objectives[]`,
  `target_audience[]`, `accreditation`, `how_to_earn_credit`,
  `estimated_time`, `release_date`, `expiration_date`, `commercial_support`,
  `disclosure_summary`, `hardware_software_requirements`, `faculty[]`
  (`name`, `degree`, `title_affiliation` — no contact details), and
  `references[]`.
- Values are byte-identical to the corresponding fields on
  `GET /api-course-content`; the accreditation block is now built by one shared
  server-side helper used by both endpoints, so the two cannot drift.
- Strictly non-gated: `overview` contains nothing from
  `phases`, `evaluation_questions`, `confidence_baseline`, `debrief`, or
  `follow_up`. Answer keys remain behind `outcomes:read` on the content
  endpoint only, and `follow_up` semantics are unchanged.
- No extra database work: `overview` is derived from the row the catalog query
  already reads, so catalog latency is unchanged.
- Envelope shape, pagination, and the `authenticated_organization_id` echo are
  unchanged.

## 2026-08-23

Additive. Provider scoping by data instead of by API-key trust.

- `organization_id` is now declared **non-null** on every `GET /api-courses`
  catalog item (including `include_unlisted=true`) and on the
  `GET /api-course-content` payload. It is the UUID of the IQ organization that
  owns and publishes the activity, stable across revisions of the same course,
  and identical to the organization an API key is scoped to. Rows without an
  owner are filtered out of the catalog rather than emitted with `null`.
- New optional envelope field `authenticated_organization_id`: the organization
  the authenticated key is scoped to, echoed alongside `contract_version` on
  `GET /api-courses`, `GET /api-course-content` and `GET /api/public/whoami`.
  Null only for legacy unscoped platform-wide keys.
- New endpoint `GET /api/public/whoami` (Bearer API key) returning
  `{ organization_id, name, scopes[] }` so a workspace can bind to exactly one
  provider organization at connection-test time.
- New golden fixture `fixtures/whoami.json`; existing fixtures re-emitted at
  this revision with the envelope echo.

## 2026-08-22

Added (all additive; nothing removed or retyped):

- `follow_up` on `CourseContentPayload` (`GET /api-course-content`) — the
  accredited outcomes-measurement instrument for the activity. **Optional**:
  activities with no published instrument omit the field entirely, and an
  instrument with no commitment options, no survey items, and no recheck is
  treated as absent and is not emitted.
- `follow_up.intervals_days` — positive integers, days after completion.
  Defaults to `[30, 60, 90]` when the author has not narrowed them. These are
  the intervals IQ Apparatus accredits for outcomes reporting; send on those
  offsets.
- `follow_up.email` (`subject`, `intro`, `cta_label`) and `follow_up.landing`
  (`title`, `intro`, `accreditation_note`) — render **verbatim**.
  `accreditation_note` is the activity's provider statement and must appear
  unmodified in both the email and the landing page; `null` means there is no
  statement to reproduce — do not invent one.
- `follow_up.commitment` (`prompt`, `options[]`, `allow_free_text`) and
  `follow_up.survey` (`items[]`, `barriers[]`). Item `id`s are stable across
  revisions — store and report them. Item types: `single`, `multi`, `scale`,
  `text`; drop unknown types. Item id `practice_changed` with option ids
  `yes` / `partially` / `no` is the headline outcomes metric when present.
- `follow_up.recheck` (optional knowledge re-check).
  **Answer-key policy:** `recheck.options[].is_correct` is emitted **only** for
  API keys holding the new `outcomes:read` scope, so no answer key can reach a
  browser. Keys without that scope receive identical options with every
  `is_correct` omitted. Grade server-side.
- New scope: `outcomes:read`. Existing keys are unaffected and keep working;
  request it only if you grade the recheck on your own server.

Decisions this revision settles:

- **Sender of record.** The consumer is sender of record — its own brand,
  sending domain, and deliverability. IQ Apparatus is author of record for the
  wording and for the accreditation statement carried in
  `follow_up.landing.accreditation_note`, which must be reproduced verbatim.
- **Accredited intervals.** 30 / 60 / 90 days after completion, unless the
  activity narrows them; always read `intervals_days` rather than hard-coding.

Serving:

- Pinned artifact for this revision:
  `/contract/iq-render-contract-2026-08-24.ts` and its `.ts.txt` mirror.
  Both are regenerated from the single source of truth, so they cannot be empty
  or drift from `/contract/iq-render-contract.ts`.
- Golden fixtures re-emitted at `2026-08-22`;
  `bramwell-series-module.json` now carries a full `follow_up` block as an
  `outcomes:read` key sees it.

---

## 2026-08-21

Added (field declarations — all already emitted or newly additive):

- `credit_designations` declared on `CourseContentPayload`. It has shipped in
  responses since 2026-08-19; only the published type was missing it.
- `series.course_id`, `series.activity_type`, `series.delivery_format` — the
  current module's identity repeated inside the `series` block so a consumer
  inspecting that block alone can locate itself in the roster. Same values as
  the top-level fields of the same names.
- `series.sequential_gating_enforced` documented explicitly as **advisory and
  always `false`**. The API will serve module N with module N−1 incomplete.
  Sequential gating is the consumer's responsibility, enforced from its own
  completion records. It is not a server-side guarantee.

Serving:

- `/contract/*.ts` is now served as `text/plain; charset=utf-8` with open CORS
  (some hosts guessed `text/vnd.trolltech.linguist`). Plain-text mirrors are
  also published at `/contract/iq-render-contract.ts.txt` and
  `/contract/iq-render-contract-2026-08-21.ts.txt`.

Reminder for consumers pinned to 2026-08-19: the `introduction` phase type was
added to the phase vocabulary and canonical order in **2026-08-20**. Re-pin to
stop logging it as unknown. The `introduction` phase carries only its own
activity content (case setup / "meet the patient"); it does not restate
`landing_page`, `learning_objectives`, or `accreditation`.

Nothing removed or retyped.

---

## 2026-08-20



Added (catalog card parity):

- `card_display_config` is now a typed object (`CardDisplayConfig`) instead of a
  loose record. New fields: `credit_chips[]` (merged by amount, with a
  ready-to-render `display`), `hide_credit_chips`, `price` (`amount`,
  `is_free`, `display`), `provider_name`, `format_line`, `cta`
  (`label`, `style`), and `summary` (plain text). Existing `image`, `badge`,
  `stripe`, and `is_featured` are unchanged.
- `series` on every catalog item: `id`, `name`, `position`, `total` (full
  published roster, not the current page), `is_series_module`. `null` for
  standalone activities.
- `SeriesCardView` interface documenting the consumer-composed series card.
- Theme: `.bw-catalog-card` and `.bw-series-card` recipes in
  `bramwell-theme.css`, scoped under `.iq-bramwell-theme`.
- Spec: sections 8 (Catalog cards) and 9 (Series rail).

Nothing removed or retyped in a breaking way; consumers pinned to 2026-08-19
keep working and simply ignore the new fields.

---

## 2026-08-19

Added:

- `contract_version` on every API response envelope, and on course-content and
  catalog payloads.
- `organization_id` on catalog items (`GET /api-courses`) and on course content
  (`GET /api-course-content`) — lets a consumer verify every item belongs to
  its key's organization.
- `accredited_provider_name` on catalog items.
- `credit_designations` and `available_certificate_types` on course content,
  matching the catalog shape. These are authoritative; never parse the prose
  accreditation statement. When `certificateSettings` is absent, the server
  parses the statement once and returns the structured result.
- `activity_details` on course content (project id, credit hours, MOC points,
  pharmacology credit, ILNA points, target audience, categories, tags).
- Per-block `expires_at` on signed video blocks, and `fallback_html` on every
  `talking-avatar` / `ai-video` block for pending, failed, and expired states.
- `media_expires_at` is now emitted whenever the payload contains any expiring
  signed media, not only avatar renders.
- `series.next`, `series.previous`, and `series.sequential_gating_enforced`
  (always `false` — the API never refuses module N because N−1 is incomplete;
  ordering is the consumer's to enforce from its own completion records).
- Golden fixtures and this changelog.

Unchanged: response envelope shape, scopes, phase and component vocabularies,
and the rule that no answer keys, rationales, or feedback ever ship in course
content.

## 2026-08-17 (pre-versioned)

- Playable signed `video_url` / `poster_url` / `caption_url` on avatar and
  AI-video blocks, with `video_status`.
- `debrief`, `references`, and `series` on course content.
- Extended answer-key stripping across newer component types.
