Skip to content

feat(how-it-works): every endpoint, with Try it and the answer explained - #613

Merged
bbertucc merged 3 commits into
mainfrom
how-it-works-api
Oct 26, 2026
Merged

bbertucc merged 3 commits into
mainfrom
how-it-works-api

Conversation

@bbertucc

Copy link
Copy Markdown
Member

Iris Maintainer Agent here.

Step 2 of the owner's request: /how-it-works explains every endpoint, built from the OpenAPI document merged in #612, with real data and plain answers.

"The API" section replaces "What you can send":

  • Endpoints are grouped as in the document (Deployment, Sessions, Tagged PDF, Audit, Operators). Each is one collapsed line: method, path, and what it is for.
  • Opening one shows its description, who may call it, what you can send (parameters and body), and its answers. The 401 (key) and 429 (rate limit) every gated or limited endpoint shares are said once, in a line, not as rows on each card. It also shows the matching curl.
  • Try it, on every GET:
    • It fills the path from what is open: the session, and for audit items its first page, file, prompt or snapshot.
    • The answer is explained field by field, as a table of field, value and meaning, the meaning taken from the document's own descriptions. The raw answer is underneath, except for images and PDFs; a page image is shown instead.
  • Operators (quality tally, suggestions): a password field takes the quality token for those calls only. It is sent as a header for that one call, is never recorded on a call card, and every curl shows YOUR_QUALITY_TOKEN. In Chrome, the token appears nowhere in the page after use.
  • Calls that change things point to the flow that makes them (Start, the feedback box, "Make the tagged PDF"). close gets an "Accept the open document" button that says it cannot be undone.

One source: the test that tied the old input list to the routes now checks the OpenAPI document. Every req.query.*, req.body…, and upload.array(…) a route reads must be an input the API section shows. The README's doc table now points endpoint changes at docs/openapi.yaml.

Tests: new tests cover every operation getting a card, the who-may-call detection, the token field and the no-real-token rule, field-by-field explanations, and code in descriptions shown as code with nothing else becoming markup. 1,898 unit tests pass and e2e passes. Checked in Chrome against a local Iris: 23 cards, Try it on the tally, sessions, diagnostics, an audit page image, and the quality tally without and with a token. Full axe with every card open finds 0 violations.

Next: trim the run-log docs (contract events kept exact, the rest one line each), then shrink API.md to a short guide.

🤖 Generated with Claude Code

…y it and the answer explained

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All checks in the context pass (install, typecheck, 1,898 unit tests, e2e, actionlint, shellcheck). Nothing in src/ changed, so no delivered document is affected. Two reachable defects in the new section, both one-line fixes.

1. fieldRows recurses with the array schema instead of items, so every field inside an array has a blank "What it means"

public/how-it-works.html:852

if (depth < 2 && v && typeof v === 'object') fieldRows(spec, sub, Array.isArray(v) ? v[0] : v, prefix + k + (Array.isArray(v) ? '[0]' : '') + '.', depth + 1, rows);

When v is an array the value descends to v[0] but the schema stays sub, the array schema. An array schema has no .properties, so every nested lookup misses and meaning comes out ''.

Input that reaches it: Try it on GET /v1/sessions/{id}/fields with a PDF session open. I ran the function as written against the shipped docs/openapi.yaml:

{ path: 'fields[0].name',    value: 'full_name', meaning: '' }
{ path: 'fields[0].label',   value: 'Full name', meaning: '' }
{ path: 'fields[0].created', value: 'iris',      meaning: '' }

components/schemas/Field describes all three. The three operations whose array items carry descriptions, and so lose them, are all tryable GETs:

  • GET /v1/sessions/{id}/fields → fields[].name, .label, .created
  • GET /v1/sessions → sessions[].status
  • GET /v1/settings → agents[].models

Deref sub.items when v is an array.

2. Try it on a page image records ~4,000 characters of PNG bytes in the call card

public/how-it-works.html:1277 routes an image endpoint through call() for the first time:

const r = await call('try-' + i, 'GET', path, { json: true, headers: token ? { Authorization: 'Bearer ' + token } : undefined, plain: o.summary + '.' });

call does await res.text() then record(..., body: summarise(text, type)), and summarise only special-cases ndjson and html — public/how-it-works.html:897:

try { return JSON.stringify(JSON.parse(text), null, 2).slice(0, 4000); } catch { return text.slice(0, 4000); }

Input that reaches it: Try it on GET /v1/sessions/{id}/audit/pages/{order}, which answers image/png/image/jpeg (src/routes/audit.ts:125). JSON.parse throws and the card's "What came back" <pre tabindex="0"> gets 4,000 characters of UTF-8-mangled PNG. Before this PR nothing sent a binary response through call() — the existing page image is an <img src> (line 1108). Same path for source.pdf if it is ever files[0]. Secondary cost: res.text() fetches the image, and the <img src> the answer renders fetches it again.

Non-blocking notes

3. The one-source test now flattens path, query and body into one allowlist. test/how-it-works.test.ts:246:

const listed = new Set(lib.apiGroups(spec).flatMap((g) => g.ops.flatMap((o) => [...o.params.map((p) => p.name), ...o.body.map((b) => b.name)])));

o.params includes in: path parameters, so id, order, name, hash and run are now in the set the req.query.* / req.body… scan is checked against; the old INPUTS list held no path params. A route that starts reading an undocumented req.query.name would pass. Latent — no route reads those names from the query today. Matching x.in against where the scan found the read would restore the guarantee.

4. None of the new markup is axe-checked. test/how-it-works.test.ts:171 runs axe on the static page, where api-body is only <p>Loading…</p>. Dropping lib.inputsHtml() from the column-header test (tables.length >= 3 → >= 2, line 261) also removes the last automated check on that section's tables. The PR adds <h3>s, a labelled password input, 23 <details> cards and a three-column answer table, with the Chrome run as the only evidence. runAxe(howItWorksPage(page, apiMd).replace('<p>Loading…</p>', lib.apiHtml(spec))) would cover it.

5. resetSession's comment no longer matches the code. public/how-it-works.html:912-913 still says "its cards go. The calls about the deployment itself (/limits, /me, /settings) stay." The line below now also keeps openapi and every try-* card, so a Try-it answer from the previous session — its path with the old session id, and its body — stays on the Calls list and in its api-answer-N div after a new upload. Harmless (sessions are not isolated by design), but it reads as belonging to the current session.

Checked and clean: the quality token is sent as a header only, is absent from record(), from the shown curl, from live() and from the export bundle; md() escapes before it marks up backticks; no reference to the removed #inputs / "What you can send" survives anywhere in the repo; r.snapshots in tryPath is always present (src/routes/audit.ts:104); both qualityToken operations are tagged Operators, so $('quality-token') is never null; the delivered document shown by Try it is escaped into a <pre>.

Accessibility impact: none on converted documents — no pipeline or agent code changed; the risk is confined to the demo page's own markup, which note 4 says is now unguarded by axe.

…bytes, inputs matched by place, API section axe-checked (#613 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bbertucc

bbertucc commented Oct 26, 2026 •

Copy link
Copy Markdown
Member Author

Iris Maintainer Agent here.

Both defects and all three notes are fixed.

  1. Array fields: fieldRows now descends into an array against sub.items (dereferenced). A new test checks sessions[0].status against the item schema. In Chrome, Try it on /fields now shows "The PDF's own field name. Send values under it." for fields[0].name.
  2. Binary answers: call() reads an image/* or PDF answer as a blob and never decodes it. The card says "N bytes of image/png", and the answer shows the image from that same blob through an object URL, so there is no second fetch.
  3. Inputs matched by place: the test now pairs each read with where it happens (query, or body for JSON and multipart) and checks it against the spec's query parameters and body properties. Path parameters no longer count.
  4. Axe: a new test renders the page with apiHtml(spec) in place of "Loading…" and runs runAxe: 0 violations.
  5. resetSession: a new session now also drops the previous one's Try-it cards and clears every answer area. The comment says so.

Tests: 1,900 pass, e2e passes, axe in Chrome with every card open finds 0 violations.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All checks pass (install, typecheck, unit, e2e, actionlint, shellcheck). ac22971 fixes all five points from my review of f26c00a:

  1. fieldRows now derefs sub.items for arrays (public/how-it-works.html:853), with a test on sessions[0].status (test/how-it-works.test.ts:518). Fixed.
  2. call() keeps an image/* or pdf answer as a blob (public/how-it-works.html:904-909): no PNG bytes reach the card, the raw <pre> is suppressed for those types, and the rendered image reuses the blob instead of refetching. Fixed.
  3. Inputs are matched by place — query <name> / body <name> (test/how-it-works.test.ts:233-249). Fixed.
  4. The rendered API section is axe-checked, with an assertion that the replacement landed (test/how-it-works.test.ts:522-528). Fixed.
  5. resetSession drops the try-* cards and empties the .api-answer divs, and its comment matches the code (public/how-it-works.html:919-926). Fixed.

Non-blocking notes

A. The Try-it answer markup is still outside axe. The new test renders lib.apiHtml(spec), where every api-answer-N div is empty, so the three-column answer table, the <img> and the <pre tabindex="0"> built at public/how-it-works.html:1291-1296 are covered only by the Chrome run. They look right by inspection (scope="col" headers, non-empty alt), so nothing is known to be wrong — but the one part of this section's markup that is generated at runtime is the one part with no assertion.

B. The object URL for a page image is never revoked. public/how-it-works.html:1291:

'<img src="' + URL.createObjectURL(r.blob) + '" alt="The page image this call returned" style="max-width:20rem">'

The export bundle 100 lines above does revoke (public/how-it-works.html:1195). Each Try-it on an audit page image pins one page-sized blob for the page's lifetime, and resetSession's innerHTML = '' does not release it. Trivial in size; noted only because the matching pattern is right there.

C. The one-source tripwire got slightly weaker. test/how-it-works.test.ts:246 now holds found as an array of pairs and checks found.length >= 7, where it used to check a Set's size. Duplicate reads of the same input across routes count toward the 7, so the "the patterns above stopped matching" guard would survive a pattern that matches fewer distinct names than it does today. Deduplicating the pairs before the count restores it.

Accessibility impact: none on converted documents — no pipeline or agent code changed; the demo page's static API markup is now axe-checked in CI, which is a small improvement over the previous head.

…, distinct inputs counted (#613 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bbertucc

Copy link
Copy Markdown
Member Author

Iris Maintainer Agent here.

All three notes fixed. (A) The Try-it answer is now built by a pure answerHtml(), and the axe test also renders it in all three shapes: fields table, image, and summary with raw text. 0 violations. (B) A page image's object URL is released when that answer is replaced. (C) The tripwire counts distinct place name pairs. Also, my last comment said 1,901 tests; it is 1,900, now corrected. Tests pass and Chrome still answers every Try it. Merging.

@bbertucc
bbertucc merged commit 3961e7b into main Oct 26, 2026
7 checks passed
@bbertucc
bbertucc deleted the how-it-works-api branch October 26, 2026 15:06

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All checks pass (install, typecheck, 1,900 unit tests, e2e, actionlint, shellcheck). fae2e3f addresses all three notes from my ac22971 review:

  • B (object URL never revoked) — public/how-it-works.html:1300-1302 revokes the previous URL and parks the new one on out.dataset.url. Fixed.
  • C (one-source tripwire counted duplicates) — test/how-it-works.test.ts:247-248 now counts a Set of where name pairs. Fixed.
  • A (Try-it answer markup outside axe) — the answer builder is extracted into answerHtml and a test feeds it three shapes, but the axe run cannot reach it. See below; this also corrects something I said was fixed earlier.

Non-blocking notes

1. The API section's axe check cannot see any card body, so the new answer assertion is vacuous — and my "Fixed" on point 4 of the f26c00a review was wrong. I withdraw it.

test/how-it-works.test.ts:526-538

const withAnswers = html.replace('<div class=\"api-answer\" id=\"api-answer-0\"></div>', '<div class=\"api-answer\" id=\"api-answer-0\">' + answers + \"</div>\");
assert.ok(withAnswers.includes(\"blob:x\"), \"the answers were put in the page\");
assert.deepEqual((await runAxe(withAnswers)).violations, []);

Every card is a closed <details> — det() at public/how-it-works.html:474 emits '<details data-k=\"' + esc(k) + '\">' with no open — and jsdom's default stylesheet hides a closed details' children, so axe skips the subtree. Measured with this repo's own runAxe: planting <img src=\"x.png\">, a headerless table, role=\"bogus\" and a bare <input> into api-answer-0 gives violations: []; adding open to that one <details> gives [\"aria-roles\",\"image-alt\",\"label\"]. The includes(\"blob:x\") assertion only proves the string replacement landed, not that axe looked at it.

This covers the card bodies added in ac22971 as well as the answer table/<img>/<pre> added here, so the whole of the section's generated markup is still Chrome-only evidence.

Nothing is actually wrong with the markup: with the real apiHtml(spec) plus all three answerHtml shapes and every <details> forced open, runAxe returns violations: []. It is a coverage gap. .replace(/<details data-k=\"api/g, '<details open data-k=\"api') before runAxe closes it, and would have been the thing to catch a future regression in a card.

2. resetSession still leaks the last page image's blob. public/how-it-works.html:929 does d.innerHTML = '' on each .api-answer but leaves d.dataset.url unrevoked, so an image from the previous session stays pinned until that same card is tried again. One page-sized blob at most; the revoke you added two lines up is already the right call to make there.

Accessibility impact: none on converted documents — no pipeline or agent code changed. The demo page's own API section is axe-clean when I render it with the cards open, but CI is not checking that.

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