Skip to content

Refresh the rubric audit against the SDK as it is now - #214

Merged
jeremy merged 19 commits into
mainfrom
refresh-rubric-audit
Oct 18, 2026
Merged

jeremy merged 19 commits into
mainfrom
refresh-rubric-audit

Conversation

@jeremy

@jeremy jeremy commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Stacked on #215. #215 moves every SDK's magic-link flow onto Fizzy's real contract and the generated routes and types. With it underneath, 1B.2, 1C.1 and 1C.3 pass, and make audit-check is green: "fresh and must-pass criteria verified". Merge #215 first. GitHub then retargets this PR to main.

The history below records how the audit found those failures.


The Rubric Check fails on main because rubric-audit.json is dated 2026-09-09, which make audit-check reports as 38 days old (the limit is 30). The repo has no generator for this file. audit-check and #161, the last refresh, treat a refresh as re-checking each criterion against the code and updating date, and that is what this PR does.

This PR does not turn Rubric Check green, and that is deliberate. The re-check found two must-pass criteria false, both since the Go SDK landed and both for the same reason: the hand-written magic-link flows. 1B.2 ("Body types generated") fails because Go, Swift and Kotlin declare their own magic-link request and response shapes. 1C.3 is quoted here:

"All paths generated from OpenAPI spec; no manual fmt.Sprintf or string interpolation for paths"

Generated service paths hold in every language. The hand-written magic-link flows in five languages build their own paths, though, and those paths aren't in openapi.json. The spec has /session.json, /session/magic_link.json and /signup/completion.json:

Language Path the flow builds Where
Go /api/v1/sessions, /api/v1/sessions/redeem go/pkg/fizzy/magic_link.go:49,98
TypeScript /sessions, /sessions/redeem typescript/src/auth/magic-link.ts:72,97
Ruby /sessions, /sessions/redeem ruby/lib/fizzy/magic_link_flow.rb:34,43
Swift /sessions.json, /sessions/redeem.json swift/Sources/Fizzy/Auth/MagicLinkFlow.swift:72,111
Kotlin /api/sessions, /api/sessions/redeem, /api/sessions/signup kotlin/sdk/src/commonMain/kotlin/com/basecamp/fizzy/auth/MagicLinkFlow.kt:89,114,143

Only Swift's signup path matches the spec. Rust uses routes::CREATE_SESSION and routes::REDEEM_MAGIC_LINK. Turning the check green by re-dating the file with 1C.3: pass: true would certify a must-pass criterion that the code fails. So the audit records it as failing, and the check stays red on 1C.3 instead of on the date.

Decision for @jeremy:

  • (a) Fix the five magic-link flows to use the generated routes, as a follow-up PR. That is a behavior change to an auth flow in five SDKs, so it isn't a small CI fix. The flows look like they would 404 against Fizzy today.
  • (b) Or accept a known-red Rubric Check until then.

Other notes corrected. Every criterion was rechecked against main at 828edf3, so nothing is carried forward unread. Where the notes and the rubric's criterion names disagreed, I used the names from the sibling hey-sdk audit, which follows the same rubric. That re-read changed 2D.5, 3A.5 and 3A.6.

Now failing:

  • 1C.1, because of the same magic-link paths.
  • 1B.4. Go's generated types use plain values with omitempty, not pointers, so an absent field reads as zero. The other five languages have real optionals.
  • 2A.6. Only Rust keeps a 10 KB error-body prefix. Go's client reads 1 MB, but its magic-link flow reads up to 50 MB. Ruby skips parsing past 1 MB. TypeScript, Swift and Kotlin have no cap.
  • 3C.2. Go and Ruby cap success bodies at 50 MB and Rust at 10 MiB. TypeScript, Swift and Kotlin have no cap.
  • 3A.5 (keyring integration) and 3A.6 (no-keyring fallback). Both exist in Go only.
  • 3C.3. TypeScript falls back to an untruncated statusText, and Go's 404 message quotes the full URL.
  • 4B.5. Of 112 operations, 39 have no conformance case of their own. The recount reproduces Rust SDK docs, examples and rubric refresh #161's 127/69 and now gives 134 cases across 6 runners, naming 73 operations.
  • 4C.4. Kotlin's release treats any 409 as a finished publish, so a rerun after a partial upload stops early. The other workflows are re-run safe.

Still passing, with corrected notes:

  • 1B.5. Rust parses *_at fields as chrono::DateTime<Utc>. The other languages keep ISO 8601 strings, and openapi.json has no format: date-time.
  • 2D.5 (resilience primitives configurable). Go and Rust apply them per client. The other four languages ship them, but the client doesn't apply them. Separately, Go's and Ruby's retry budgets ignore the per-operation retry metadata.
  • 3C.4. Hooks never receive headers.
  • 1A.6, 1B.2 and 3A.4 were rechecked and still hold.

Copilot AI balanced review requested due to automatic review settings October 17, 2026 20:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T01:48:57.748818Z e1515aa New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: a1fba3b3fb

ℹ️ 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 rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
Comment thread rubric-audit.json

Copilot AI 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.

🟡 Changes recommended

The refreshed audit still contains factual inaccuracies that undermine its purpose as an accurate SDK assessment.

3 open issues
What changed in this PR

Refreshes the SDK rubric audit to reflect current implementation evidence, deliberately leaving Rubric Check red for failing criterion 1C.3.

Changes:

  • Updates the audit date to October 17, 2026.
  • Revises route-generation, body-limit, resilience, and release notes.
  • Updates conformance counts to 134 cases across six runners.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
rubric-audit.json Refreshes audit date, criterion results, and supporting evidence.

Review effort: Balanced

Comment thread rubric-audit.json Outdated
Comment thread rubric-audit.json
Comment thread rubric-audit.json Outdated

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: fdfbe92c62

ℹ️ 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 rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
@jeremy

jeremy commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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

Here are some automated review suggestions for this pull request.

ℹ️ 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 rubric-audit.json
Comment thread rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
@jeremy

jeremy commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Review threads: 15 resolved (15 fixed, 0 declined), over four rounds: 6, then 3, 3 and 3. Each round corrected a note, and every finding was true.
Open for a decision: @jeremy needs to decide what to do about the hand-written magic-link flows. They now fail two must-pass criteria: 1C.3, because they build their own paths, and 1B.2, because they declare their own request and response shapes, such as Go's CreateSessionRequest{email} against the generated email_address / pending_authentication_token. Rubric Check stays red until the flows move onto the generated routes and types (a follow-up PR in Go, TypeScript, Ruby, Swift and Kotlin) or that's accepted.
Instrument note: findings have held at 3 per round for three rounds, all on how strictly a note words a pass. That says more about the audit format than about any single note. One date and one pass/fail per criterion across six languages invites an unending list of per-language exceptions. Every criterion now reflects main at 828edf3. I'm stopping note-precision rounds here. A further finding of the same kind is better answered by changing the format (per-language verdicts, or per-criterion dates) than by another edit.

@jeremy
jeremy force-pushed the refresh-rubric-audit branch from d32efc9 to 8139cba Compare October 17, 2026 23:22

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 8139cba91b

ℹ️ 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 rubric-audit.json Outdated
…ions

Sessions::MagicLinksController reads params[:code], not token, and
reads the pending sign-in from the pending_authentication_token cookie
CreateSession sets; CreateSession answers 201 and DestroySession 204.
The model said token, 200 and 200, so nothing generated from it could
complete a sign-in. RedeemMagicLinkInput now carries code, the two
statuses are modelled, and the operations' docs say which cookie each
side of the exchange carries. Every language regenerates; the
conformance fixtures and runners send code and expect 201 and 204, and
Rust's names.toml keeps the code redacted as the token was.
MagicLinkFlow posted {email} to /api/v1/sessions and {token} to
/api/v1/sessions/redeem, neither of which Fizzy routes, and dropped the
pending token the answer carries, so it could not sign anyone in. It
now sends through the generated SessionsService: CreateSession posts
{email_address} to /session.json and keeps the pending token from the
201; RedeemMagicLink posts {code} to /session/magic_link.json with the
token as the pending_authentication_token cookie, escaped as Rack
unescapes it, and answers the generated SessionAuthorization.
PendingToken and ResumeMagicLinkFlow let a sign-in finish in another
process, as Rust's flow does. Like NewClient, the flow now requires
HTTPS outside localhost.

This breaks the flow's API: CreateSession answers
*generated.PendingAuthentication, RedeemMagicLink takes the emailed
code and answers *generated.SessionAuthorization, Login answers that
session too, and the hand-written CreateSessionRequest/Response and
RedeemMagicLinkRequest/Response types, with their user_id, are gone.
fizzy-cli does not use this flow.
MagicLinkFlow posted {email} to /sessions and {token} to
/sessions/redeem, which Fizzy has never routed, and discarded the
pending token CreateSession answers. It now goes through the generated
sessions operations on a client from createFizzyClient: CreateSession
with {email_address}, keeping the 201's pending token; RedeemMagicLink
with {code} and the pending token as a percent-encoded
pending_authentication_token cookie, which is where Fizzy reads it; and
CompleteSignup with {full_name} under the session cookie.

This changes the API: requestLink(email) is createSession(emailAddress)
and answers the pending token; redeemToken(token) is redeem(code) and
answers requiresSignupCompletion beside the session token and auth;
resume(pendingToken) and pendingToken let a sign-in finish in another
process; completeSignup(fullName) is new. The options take the client
options that apply before there is a credential (baseUrl, userAgent,
requestTimeoutMs, hooks).
MagicLinkFlow posted {email} to /sessions and {token} to
/sessions/redeem, neither of which Fizzy routes, and never sent back
the pending_authentication_token cookie Fizzy reads the sign-in from,
so no sign-in could complete. It now goes through the generated
SessionsService: create_session(email_address:) posts /session.json,
expects the 201's pending token and keeps it; redeem(code:) posts
/session/magic_link.json with {code} and the token as a percent-encoded
Cookie, and answers session_token and requires_signup_completion.
pending_token hands the token out and pending_token: resumes a sign-in
another process started. request_magic_link(email:) and redeem(token:)
are gone; they could not have worked.
MagicLinkFlow posted {email} to /sessions.json and {token} to
/sessions/redeem.json, which Fizzy does not route, decoded a
pendingToken Fizzy never sends, and completed signup with {name} into a
body Fizzy does not answer. It now goes through the generated
SessionsService: start(emailAddress:) posts {email_address} to
/session.json and keeps the 201's pending token, redeem(code:) posts
{code} to /session/magic_link.json with the token as an explicit,
percent-encoded pending_authentication_token cookie and no cookie store
in play, and completeSignup(fullName:sessionToken:) posts {full_name}
and takes the empty 201. pendingToken and resume(pendingToken:) let a
flow finish in another process, as Rust's does.

This changes the flow's API: start(email:) is start(emailAddress:) and
answers PendingAuthentication, redeem(token:) is redeem(code:) and
answers SessionAuthorization, and completeSignup takes fullName and
answers nothing; PendingSession, RedeemResult and SignupResult are gone.
MagicLinkFlow posted to /api/sessions, /api/sessions/redeem and
/api/sessions/signup, which Fizzy does not route, redeemed with the
pending token in the body where Fizzy wants the emailed code and the
token as a cookie, and named a new user with name rather than
full_name. No sign-in could complete.

The flow now runs on the generated Sessions operations through a
FizzyClient, so the paths, bodies, statuses, error mapping, HTTPS check
and body limits are the SDK's own: requestMagicLink posts
/session.json with email_address and keeps the pending token from the
201; redeemMagicLink takes the code and sends it to
/session/magic_link.json with the pending token as a percent-encoded
pending_authentication_token cookie; completeSignup posts full_name to
/signup/completion.json on the session cookie and takes the empty 201.
A sign-in finishing in another process resumes from pendingToken, as a
constructor argument or through resume; redeeming with none pending is
a usage error.

Breaking: redeemMagicLink takes the emailed code, not the pending
token, and completeSignup's second argument is the full name.
…code

The flow sent a private {code, token} body so it worked against Fizzy
and against a server built from the model, which spelled the code
token. The model now says code, so the flow sends the generated
RedeemMagicLinkRequestContent, still with the pending cookie the model
documents but cannot send, and the module docs say that instead of
where the model was wrong.
…t's cache

The client caches an AccountClient that holds the client, and the
account caches the service that holds it, so a client built per request
and reached through .sessions was never freed, nor the cookie it signed
with. The flow now builds SessionsService(accountClient:) directly, and
a test checks a released flow lets go of its transport.
Ruby, Kotlin and Swift kept the pending token after a successful
redeem, so pendingToken reported a consumed sign-in and a second redeem
sent it to Fizzy instead of failing locally. Go, TypeScript and Rust
cleared it unconditionally, which erased a newer sign-in a create
started while the redeem was in flight. Each flow now clears the token
only if it is still the one it redeemed, and only after success, so a
refused code can be retried. Tests assert the spent token and the local
usage error on a second redeem.
@jeremy
jeremy force-pushed the refresh-rubric-audit branch from 9eff99c to d5bb686 Compare October 18, 2026 01:06
@jeremy
jeremy changed the base branch from main to fix-magic-link-flows October 18, 2026 01:06

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: d5bb686932

ℹ️ 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 rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
Comment thread rubric-audit.json Outdated
… state

Kotlin's and Ruby's auth strategy read the flow's pending token when the
request was signed, after hooks had run, so a sign-in started or resumed
meanwhile could pair this code with another's cookie; Fizzy spends the
code before refusing the mismatch. Each redeem now signs with a client
bound to the token it read, and the pending state sits behind an atomic
(Kotlin) or a mutex (Ruby). A Ruby test starts a second sign-in from a
hook mid-redeem and checks the cookie and the surviving pending token.
@jeremy
jeremy force-pushed the refresh-rubric-audit branch from d5bb686 to e8c9b97 Compare October 18, 2026 01:26
An older redeem finishing after a newer one overwrote the flow's stored
session, so an implicit completeSignup could sign one person's sign-up
under another's session. The flow now keeps no session: completeSignup
takes the token redeem answered, as every other SDK's flow already does.
The audit was 38 days old, past audit-check's 30. Re-checking each
criterion against main found notes that no longer, or never, matched
the code: error-body and success-body limits differ by language and
three languages have none, resilience is wired into the client only in
Go and Rust, hooks never see headers, the fixtures now hold 134 cases
naming 73 operations across six runners, and the hand-written
magic-link flows in five languages build their own paths, off the
spec's, rather than using the generated routes. The notes now say what
the code does and the criteria it fails are marked failing, which
includes the must-pass 1C.3.
Rust parses *_at fields as chrono DateTime (1B.5); Go's generated types
use plain values with omitempty, not pointers (1B.4, now failing); Go's
magic-link flow reads error bodies up to 50 MB (2A.6); only parsed
server messages are bounded at 500 bytes, and Go's 404 quotes the full
URL (3C.3). 2D.5, 3A.5 and 3A.6 are re-read against the rubric's names
(resilience configurable, keyring integration, the no-keyring
fallback): keyring storage and FIZZY_NO_KEYRING exist in Go only. 1A.6,
1B.2 and 3A.4 were rechecked and still hold, so no criterion is carried
forward unread.
TypeScript's error fallback keeps statusText untruncated; 39 of 112
operations have no conformance case of their own; Kotlin's release
treats any 409 as a finished publish, so a rerun after a partial upload
leaves publications unpublished.
… notes

The hand-written magic-link flows declare their own request and response
shapes, the same root cause that fails 1C.1 and 1C.3. Go has one
generated pointer (Column.Color), and the rate limiter is per client,
not per operation.
Rust's flow sends generated types over generated routes through
client.operation rather than the generated service; Go's magic-link
flow no longer reads error bodies at the success limit; and Go's
WithResilience installs gates no request path consults, so 2D.5 fails.
@jeremy
jeremy force-pushed the refresh-rubric-audit branch from e8c9b97 to e1515aa Compare October 18, 2026 01:44

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: e1515aaf9f

ℹ️ 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 rubric-audit.json
@jeremy

jeremy commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Review threads: 19 resolved (18 fixed, 1 declined).
Declined: Ruby's generated services build hashes rather than using the types.rb models. That doesn't make any body hand-constructed. The reasoning is in the thread.
Now stacked on #215. With #215 underneath, 1B.2, 1C.1 and 1C.3 pass, and Rubric Check is green on e1515aa. Codex and Copilot have reported on that head.
Open for a decision: @jeremy needs to decide whether to land this after #215.

Base automatically changed from fix-magic-link-flows to main October 18, 2026 04:02
@jeremy
jeremy merged commit ab5f737 into main Oct 18, 2026
34 checks passed
@jeremy
jeremy deleted the refresh-rubric-audit branch October 18, 2026 04:02
@jeremy jeremy mentioned this pull request Oct 19, 2026
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.

2 participants