Skip to content

v1.21 batch: Phases 91-96 verified (B2B-01..03, HARD-01..02, HARD-03/API-01)#37

Open
szTheory wants to merge 305 commits intomainfrom
chore/phase-88-uat-evidence
Open

v1.21 batch: Phases 91-96 verified (B2B-01..03, HARD-01..02, HARD-03/API-01)#37
szTheory wants to merge 305 commits intomainfrom
chore/phase-88-uat-evidence

Conversation

@szTheory
Copy link
Copy Markdown
Owner

@szTheory szTheory commented Apr 28, 2026

Summary

Branch chore/phase-88-uat-evidence has accumulated the full v1.21 milestone — six phases covering org MFA enforcement, RBAC seams, M2M service-account tokens, the Postgres-only declaration, optional-dep boot validation, and OAuth refresh + rate-limit headers. All six phases have passed goal-backward verification.

The headline new addition since the previous PR description is Phase 93 — M2M / service-account tokens (B2B-03), freshly re-verified at 22/22 must-haves with all gaps closed.

What's in this PR

Multi-phase batch — newest first:

Phase 96 — OAuth refresh + rate-limit headers (HARD-03 + API-01) — verified ✓

  • 96-01 Per-provider OAuth refresh dispatch and classification (GitHub / Apple / Facebook / Generic).
  • 96-02 Persist OAuth token refresh with atomic oauth.token_refreshed audit co-fate.
  • 96-03 Emit authoritative X-RateLimit-Limit / -Remaining / -Reset and Retry-After headers from Hammer state.
  • 96-04 Wire rate-limit + OAuth refresh into the active request seams.
  • Tests: 41 OAuth + 20 rate-limit + 56 generator-wire + 5 example-app, all 0 failures. Stub removed (rg "not yet implemented" lib/sigra/oauth.ex → no matches).

Phase 95 — Optional-dep boot validation + mix sigra.doctor (HARD-02) — verified ✓

  • Each optional dep (Oban / Bcrypt / EQRCode) raises a clear, actionable error at first use when missing instead of compiling to silent nil.
  • mix sigra.doctor reports per-feature dep status.
  • CI matrix toggles each optional dep off and verifies behavior.
  • Local merge gate: PASS. Workflow contract updated on .github/workflows/ci.yml.

Phase 94 — Postgres-only declaration (HARD-01) — verified ✓

  • mix sigra.install refuses to run against a non-Postgres adapter with a clear error.
  • Unimplemented MySQL / SQLite migration branches removed from generator templates.
  • README.md, guides/introduction/{getting-started,installation}.md, .planning/PROJECT.md, and mix.exs description all state PostgreSQL as the only supported adapter.
  • Adjacent templates (94-03) and migration templates (94-02) collapsed to Postgres only; install-time enforcement landed in 94-01.

Phase 93 — M2M / service-account tokens (B2B-03) — verified ✓ (22/22, re-verified 2026-05-02)

  • 93-01..05 Core capability: client_credentials grant on the existing JWT path, current_scope.actor_type distinction, audit rows distinguishable from user-tied tokens, generated host wiring for issue / list / revoke.
  • 93-06 D-AUD-08 co-fated rollback proof for all five SA mutations (audit insert and SA mutation share a single Repo.transact/2 so a failed audit aborts the mutation).
  • 93-07 Service-account JWT + FetchBearer parity tests covering scope-build for SA tokens vs. user tokens.
  • 93-08 SA generator gating test + fixes for blocking infrastructure bugs surfaced during the gating run.
  • 93-09 Full UI-SPEC parity for OrganizationServiceAccountsLive (template + :show route + example-app mirror + CopyToClipboard hook source + asset injection + installer wiring).
  • 93-10 E2E lifecycle test proving ROADMAP SC#4 (issue → use → list → revoke → use-after-revoke fails).
  • Re-verification: previous status was gaps_found (18/22, 3 open gaps). All three gaps closed: Postgrex.Error struct match (lib/sigra/service_accounts.ex:229,414), nil-and BadBooleanError fix in organization_service_accounts_live.ex:488,650, CHANGELOG [Unreleased] carries the B2B-03 trace bullet. MIX_ENV=dev mix compile --warnings-as-errors exits 0; E2E test passes.

Phase 92 — RBAC seams (B2B-02) — verified ✓

  • 92-01 Library RBAC seams de-opinionated. Sigra.Authz behaviour (single can?/3 callback, no built-in policy). lib/sigra/admin/policy.ex, lib/sigra/organizations.ex, lib/sigra/plug/require_membership.ex, lib/sigra/organizations/invitations.ex consume host-supplied roles only.
  • 92-02 Generated host RBAC contract. <App>.SigraAuthz allow-all starter; nullable role :string migrations; Sigra.Ecto.Types.RoleAtom for atom round-trip; dead :require_org_owner pipeline removed.
  • 92-03 Runtime role propagation. current_scope.role populated from membership.role through Sigra.Scope.Hydration and Sigra.Plug.PutActiveOrganization. Plug ↔ on_mount parity via shared hydrator. Nil-user / stale / no-org branches leave role nil.
  • 92-04 RBAC recipe (guides/recipes/role-based-access-control.md, 252 lines) registered with ExDoc, walks adopters allow-all → deny-by-default.
  • Code review: 14 findings → 0 BLOCKERs after 3 review rounds + 3 follow-up fixes. 6 advisory WARNINGs remain, all classified non-blocking.

Phase 91 — Org-level MFA enforcement (B2B-01) — verified ✓

  • Org admins can require MFA for every member of an organization with an atomic organization.mfa_policy_change audit row.
  • Unenrolled members are blocked at the HTTP and LiveView boundaries until they enroll.
  • Audit emitted via Sigra.Audit.log_multi_safe/3 from Sigra.Organizations.set_mfa_policy/5 — no new log_safe/3 debt.
  • Full library suite green at verification time: 33 doctests, 3 properties, 2214 tests, 0 failures.

v1.20 GA UAT evidence + Phase 87/88 close-out

Branch was originally opened for Phase 87/88 UAT; v1.20 GA UAT evidence accumulated alongside.

Verification

  • Phase 91 verification: passed — see .planning/phases/91-org-level-mfa-enforcement-b2b-01/91-VERIFICATION.md
  • Phase 92 verification: passed (5/5 must-haves, 0 gaps) — see .planning/phases/92-rbac-seams-b2b-02/92-VERIFICATION.md
  • Phase 93 verification: complete (22/22 must-haves, 0 gaps, re-verified 2026-05-02) — see .planning/phases/93-m2m-service-account-tokens-b2b-03/93-VERIFICATION.md
  • Phase 94 verification: VERIFIED (with environmental caveat for Oban.Worker compilation under Elixir 1.19.5, pre-existing on main) — see .planning/phases/94-postgres-only-declaration-hard-01/94-VERIFICATION.md
  • Phase 95 verification: complete — see .planning/phases/95-optional-dep-boot-validation-mix-sigra-doctor-hard-02/95-VERIFICATION.md
  • Phase 96 verification: complete — see .planning/phases/96-oauth-refresh-rate-limit-headers-hard-03-api-01/96-VERIFICATION.md
  • Last full Phase 92 test run at verification time: 2278/2278 pass, 0 regressions
  • Phase 92 code review: 3 rounds, 0 BLOCKERs, 6 advisory WARNINGs (non-blocking, tracked)
  • Phase 93 UAT: 4 automated checkpoints pass; SECURITY threat verification recorded; T-93-06 deferral notes wired

Key decisions

  • Library is taxonomy-agnostic (Phase 92). roles and owner_role are required: true config options; the library raises KeyError with an actionable message if a host calls into an RBAC-aware function without supplying them.
  • Generated host owns the role taxonomy (Phase 92). Starter is [:owner, :admin, :member] for DX, but framed as edit-to-customize.
  • Allow-all can?/3 is the SC1-mandated default starter (Phase 92). The recipe shows the deny-by-default hardening path.
  • Single authoritative :role write seam (Phase 92). Sigra.Plug.PutActiveOrganization writes scope.role after the host scope_module callback.
  • actor_type distinguishes service-account vs. user tokens (Phase 93) — same JWT path, different audit shape, different revoke semantics.
  • SA mutations are co-fated with their audit rows (Phase 93, D-AUD-08). A failed audit insert aborts the mutation via Repo.transact/2.
  • Postgres is the only supported adapter (Phase 94). Install-time refusal + docs/metadata aligned. MySQL/SQLite branches removed from templates.
  • Optional deps fail loud, not silent (Phase 95). Missing Oban / Bcrypt / EQRCode raise actionable errors at first use; mix sigra.doctor is the diagnostic surface.
  • OAuth refresh dispatched per-provider with invalid_grant classification (Phase 96). oauth.token_refreshed audit co-fated with token rotation.
  • Deferred (Phase 92): DEF-92-02-01InvitationAcceptLive Multi step-name collision, pre-existing bug from 2026-04-15. Tracked for a follow-up phase.
  • Deferred (Phase 93): T-93-06 deferral notes recorded in 93-VERIFICATION.md.

Test plan

  • CI green
  • Verify mix sigra.install against a fresh host produces the expected SigraAuthz module + nullable role column + service-account scaffolding + Postgres-only refusal on a non-Postgres adapter
  • Verify current_scope.role and current_scope.actor_type populate correctly in a generated host (manual smoke)
  • Verify mix sigra.doctor output reports per-feature dep status correctly with each optional dep toggled off
  • Verify rate-limit headers emit on POST /users/log_in up to the limit and that the 11th request 429s with all headers present
  • Skim the RBAC recipe and the SA generator output end-to-end for adopter clarity

🤖 Generated with Claude Code

szTheory and others added 30 commits April 25, 2026 17:29
- Add optional session-store transaction callbacks with default Ecto support
- Route impersonation start/stop through transactional multi-paths when available
- Mark Phase 9 and SEED-002 as closed by Phase 85
- Refresh the AUD-04 inventory and atomicity defaults to match the new boundary
- Record the merge-gate outcome and requirement coverage
- Cite the exact evidence paths for the impersonation atomicity closure
- Record the completed phase in state and roadmap artifacts
- Capture both plan summaries plus the merge-gate verification file
Adds the planning artifacts (CONTEXT, RESEARCH, PATTERNS, VALIDATION,
01-PLAN) that were produced during phase 84 but never staged. Only the
SUMMARY and VERIFICATION made it into git at the time, leaving the
planning trail incomplete on disk vs in the repo. No content change to
the phase outcome — the work shipped as commits aea70a0 / 6367344 /
e70581d / ed09505.

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

Captures the gsd-audit-milestone output for v1.19 (JWT refresh
persistence + audit co-fate, MFA enrollment failure). Verdict is
tech_debt — 7/7 requirements satisfied, 2/2 phases passed, integration
and flows green; remaining items are non-blocking test-harness and
planning-truth debt (shared audit_events table across 82/83 suites,
missing requirements-completed YAML on summaries, stale AUD-04-022
narrative in MILESTONES.md, partial Nyquist posture).

Pairs with the 82-VERIFICATION status flip: a focused Postgres rerun on
2026-04-25 of jwt_refresh_audit_cofate_test.exs passed cleanly (5
tests, 0 failures), so the verification doc moves from pending → passed
with the rerun evidence noted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds scripts/release/sync_release_summary.sh and wires it into both the
release-please workflow (after the Release PR merges) and the
hex-publish manual-recovery workflow. The script extracts the
### Summary block from the matching CHANGELOG.md release entry and
writes it into the GitHub Release body between fenced markers, creating
the release if Release Please did not. Hex publish now waits on the
sync job so a published version always has a human-readable summary on
its release page.

Codifies the convention in MAINTAINING.md (every shipped version starts
with a short ### Summary block: what changed, why it matters, action
required) and updates docs/NEXT-STEPS-MANUAL.md to point hand-cut
releases at the same summary block.

Includes the 2026-04-25 planning note that flagged the version drift
(hex.pm at 0.2.0 vs repo at 0.2.4 with Unreleased changes above the
0.2.4 entry, so HEAD should publish as 0.2.5) which motivated this
tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n automated visual regression harness

Reshapes Phase 86 from "execute manual MUA matrix" to "build automated visual
regression harness whose green-on-tag-SHA is the evidence." 0 human UAT for
v1.20 launch. Four-layer pipeline: (L1) extended ExUnit + new Sigra.A11y.Contrast
+ Example.EmailAssertions covering 9 coverage gaps; (L2) Premailex CSS-inline
+ Playwright pixel-diff Chromium+WebKit × light+dark = 36 baselines; (L3)
caniemail.com CSS feature lint; (L4) optional Mailtrap Sandbox API spam-score.
Hybrid evidence layout (in-repo manifest + hero PNGs + release-asset full bundle).
CTA contrast bumped #2563eb → #1d4ed8 (5.17:1 normal-text WCAG AA). REQUIREMENTS.md
GAUAT-01/02 + ROADMAP.md Phase 86 reworded in same commit so planner inherits
corrected scope. Launch claim downgraded from "real-mail-client tested" to honest
"render-tested across Chromium + WebKit engines × light + dark, with caniemail-
validated CSS for Gmail web / new Outlook web / Apple Mail; legacy Outlook
Word-engine desktop documented as out-of-scope (Microsoft EOL Oct 2026)."

Files:
- .planning/phases/86-gauat-email-visual-qa-phase-04-phase-08-templates/86-CONTEXT.md (new)
- .planning/phases/86-gauat-email-visual-qa-phase-04-phase-08-templates/86-DISCUSSION-LOG.md (new)
- .planning/REQUIREMENTS.md (GAUAT-01, GAUAT-02 reworded)
- .planning/ROADMAP.md (Phase 86 summary bullet + detailed section reworded)

Backed by 4 parallel research subagents (delivery+trigger, per-template rubric,
evidence layout, coverage scope verdict). Litmus/Email-on-Acid rejected on cost
($500/mo Enterprise post-Sept-2025) AND structural shape (vendor renderers can't
test app-customized templates from a hybrid lib+generator architecture). 0 quarterly
external commitment — fake commitment is worse than none.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RED phase: 13 tests covering relative_luminance/1, ratio/2, WCAG thresholds
- Verifies #1d4ed8 >= 4.5:1 (D-86-07 CTA bump), #dc2626 >= 4.5:1 (red-emphasis lock)
- Verifies malformed color error handling
- Sigra.A11y.Contrast: WCAG 2.2 relative_luminance/1 and ratio/2 using
  W3C linearization formula; handles 6-digit hex with error tuple on
  malformed input
- Example.EmailAssertions: G1-G6 shared helpers (assert_cta_contrast,
  assert_under_gmail_clip, assert_text_part_mirrors_html, assert_email_to,
  assert_xss_escaped, assert_no_outlook_landmines)
- contrast_test.exs: 13 AAA-flat unit tests including D-86-07 threshold
  (#1d4ed8 >= 4.5:1, #dc2626 >= 4.5:1, old color comparison)
- Fix test: #2563eb actually yields 5.17:1 (plan doc had 4.36 — WCAG
  formula difference); updated to assert #1d4ed8 > #2563eb (both clear
  4.5 but new color is stronger at 6.70:1)
- RED phase: 11 tests covering lint/1 and allowlist/0 API
- Verifies denial of display:flex, display:grid, position:, background-image:, <style> blocks
- Verifies allowlist structure includes gmail-web, outlook-web-new, apple-mail-macos
- Verifies safe HTML passes lint without violations
- Sigra.Email.CssLint: deny-list gate for display:flex/grid, position:,
  background-image:, and <style> blocks; loaded from vendored JSON at
  compile time (zero I/O during CI)
- priv/sigra/email/caniemail-allowlist.json: curated policy for Gmail web,
  new Outlook web, and Apple Mail macOS; MIT-licensed caniemail data
- cta_button/2: bump from #2563eb to #1d4ed8 (D-86-07); contrast is now
  6.70:1 vs white (unambiguous WCAG AA pass for normal text)
- test/example emails.ex: apply same #1d4ed8 bump to generated example app
- css_lint_test.exs: 11 unit tests covering lint/1 and allowlist/0 API
- emails_security_html_test.exs: extended to 74 tests (+56) closing G1-G9
  for suspicious_login_email, lockout_notification_email, and
  password_changed_email; includes XSS escaping tests for ip/geo_city/device
  fields, G8 default-arg DateTime.utc_now/0 branch coverage, caniemail lint
- emails_lifecycle_html_test.exs: extended to ~90+ tests closing G1-G9
  across all Phase 08 templates; G4 recipient correctness asserts that
  email_change_confirmation goes to NEW address (security-critical); G9
  backup_code_used_email at remaining: 1, 2 (low warning shown) and 3
  (first safe boundary — no warning); caniemail lint for all 7 templates
- email_assertions.ex: remove dead is_placeholder/1 dead code path per
  Elixir type checker warning
- 121 total tests in example app email files, all passing
…ity guardrails

- Sigra.A11y.Contrast WCAG 2.2 contrast calculator
- Sigra.Email.CssLint with caniemail-allowlist.json policy
- CTA color bump #2563eb -> #1d4ed8 (6.70:1 vs white)
- Example.EmailAssertions G1-G6 shared helpers
- 121 email tests closing G1-G9 rubric for all 9 templates
Plan 86-01 bumped the default CTA color from #2563eb to #1d4ed8 in
priv/templates/sigra.install/core/emails.ex (D-86-07: stronger 6.70:1
contrast vs 5.17:1 for unambiguous WCAG 2.2 AA pass). Two cross-plan
fixtures still encoded the old color and surfaced as a post-merge
regression:

- test/sigra/install/generator_email_test.exs:82 — assertion bound
  to the old hex; updated to assert the bumped #1d4ed8.
- test/fixtures/install_golden/tree/.../accounts/emails.ex:935 —
  install golden fixture; surgical 6-byte color update keeps every
  other byte identical so the byte-for-byte diff still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add mix sigra.email.snapshot with D-86-04 frozen fixtures (203.0.113.42, 2026-04-17 12:00:00Z)
- Add mix sigra.uat.report with D-86-06 manifest schema (git_sha, snapshot_sha256, contrast_min_ratio, byte_size, artifact_url)
- Both tasks support CI-safe --check mode that exits 0 without mutating outputs
- Add premailex ~> 0.3 to test/example/mix.exs for CSS inlining in snapshot subprocess
- Add email-visual.spec.ts: 9-template loop, parseProject() extracts
  engine+theme from project name, opens file:// HTML, fullPage screenshot
  with maxDiffPixels 50 per D-86-03 and D-86-11
- Add 4 new Playwright projects in playwright.config.ts:
  email-visual-{chromium,webkit}-{light,dark}, each with per-project
  expect.toHaveScreenshot.pathTemplate routing to __snapshots__/
  email-visual.spec.ts/ directory; viewport 640x1200, colorScheme scoped
- Commit 36 PNG baselines (9 templates × 2 engines × 2 themes);
  Playwright sanitizes __ to - in {arg} token so filenames use single
  dashes (e.g. lockout-notification-chromium-light.png)
- Commit 9 pre-rendered HTML files in priv/email_snapshots/ (D-86-04
  frozen fixtures, generated by mix sigra.email.snapshot)
- Fix sigra.uat.report.ex to look for {template}-{engine}-{theme}.png
  (single-dash) filenames matching actual Playwright output; both
  --phase=04 (8/8) and --phase=08 (28/28) now report full presence
- Add byte-budget.csv generation to sigra.uat.report task (D-86-06)
- Run mix sigra.uat.report --phase=08 to generate README/manifest/reports
- Copy 28 canonical Wave 2 baselines to snapshots/ with __sha-6ce3cd3 suffix
- 7 lifecycle templates × chromium|webkit × light|dark = 28 hero PNGs
- All 28 Phase 08 cells present; disposition: pass; git_sha: 6ce3cd3
- 1/1 tasks complete: Phase 08 evidence bundle materialized
- 28 SHA-suffixed hero PNGs for 7 lifecycle templates × 2 engines × 2 themes
- Deviation: added byte-budget.csv generation to sigra.uat.report (Rule 2)
- All GAUAT-02 success criteria met; disposition: pass; git_sha: 6ce3cd3
…N, and residual-policy docs

- Add email_visual_regression job to ci.yml (snapshot + report + playwright lanes, artifact upload, tag-time v1.20.0 release promotion)
- Update docs/uat-ci-coverage.md SEED-1/2 residual columns to reference email_visual_regression job; demote GA-02 v1.4 human-MUA claim to historical-only
- Update sigra.uat.report.ex to emit git_tag/ci_run_url/ci_workflow frontmatter fields and byte-budget.csv report
- Create .planning/uat-evidence/v1.20/INDEX.md enumerating email-phase-04 and email-phase-08 evidence directories
- Create 86-VERIFICATION.md with phase-close SHA 6ce3cd3, snapshot count 36, contrast min ratio 4.5, byte budget max 100000, and GAUAT-01/02 PASS attestation
… hero PNGs

- README.md with YAML frontmatter (phase, gauat_requirement, hex_version, git_sha, git_tag, ci_run_url, ci_workflow, generated_by, generated_at, disposition: pass)
- manifest.json with per-cell SHA-256, byte size, contrast min ratio, byte budget max (all 8 cells pass)
- reports/contrast-summary.json and reports/byte-budget.csv generated via mix sigra.uat.report --phase=04
- 8 hero PNGs: lockout-notification + suspicious-login x chromium/webkit x light/dark, all sha-6ce3cd3 suffix
- Hero PNGs copied from canonical Wave 2 baselines and renamed per D-86-06 double-underscore contract
szTheory and others added 30 commits May 2, 2026 15:56
Phase 93 added priv/templates/sigra.install/core/oauth_token_controller.ex
(registered in lib/sigra/install/features/core.ex Features files manifest)
but the templates_layout_test cardinality check and @manifest_post_move
list were not updated to match. Both assertions now reflect reality.

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

The Oban worker declares queue: :sigra_lifecycle (lib/sigra/workers/token_cleanup.ex:22)
but the test still asserted "sigra_mailer". Updates the test name and
expected queue string to match the implementation's actual queue.

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

The Oban-absent branch in `Core.post_instructions/2` calls
`optional_dependency_remediation(:async_email)`, which emits
"Add {:oban, "~> 2.17"} to your mix.exs deps, run mix deps.get, and
configure the sigra_mailer queue." There is no "To enable async delivery"
preamble in the impl. Updates the test to anchor on the actual remediation
marker ("Add {:oban") and the configuration intent ("sigra_mailer queue"),
preserving the original three semantic checks: detected, mode, remediation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 3accda8 added pipe_through :auth_rate_limit to the generated router
but missed defining the matching pipeline block. Every fresh `mix sigra.install`
emitted a router that failed compile with `undefined function auth_rate_limit/2`.
Restores parity with test/example/lib/example_web/router.ex (line 65–67).

The new pipeline is inserted between :require_sudo and :require_org so the
ordering matches the canonical example router.

Also regenerates test/fixtures/install_golden/ via
`mix sigra.fixture.rebless_golden`. The fixture had been stale since before
3accda8 plus accumulated drift from Phase 91/93 generator template edits
(scope.ex service_account_id, accounts.ex EEx whitespace, migration
trailing-blank stripping). Regen captures all of it together — the rebless
delta-report flagged 6 modified files and the golden_diff_test now passes.

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

Quick task 260502-lzl wrap-up. Executor landed 4 atomic fix commits
(ac746d5, 2d8bf60, 5fb711c, 043fb78) plus 2 documented no-ops; local
suite 2357/2358 passing. Single residual is a pre-existing
phx.server-spawning UpgradeIntegrationTest unrelated to this batch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s raise tagged error

Phase 93 plan 08 wrapped each worker in OUTER `if Code.ensure_loaded?(Oban.Worker)
do defmodule ... end`, which makes the entire module disappear when Oban is absent
(UndefinedFunctionError on Worker.new/2). The test contract at
test/sigra/workers/optional_deps_test.exs:32 (and test/sigra/delivery_test.exs:189
for EmailDelivery) requires the module to STAY loadable and the first queue-backed
call to raise Sigra.OptionalDeps.MissingDependencyError tagged with the appropriate
feature (:lifecycle_jobs or :async_email).

Use a dual-defmodule shape with the conditional at the OUTER level: when Oban is
loaded, define the full Oban-backed worker; when absent, define a stub module
exposing only `new/2` that calls OptionalDeps.ensure_available!/2 with the matching
feature so the tagged error fires. This keeps the module loadable in both branches
and preserves the path-dep compile-without-Oban story Phase 93 plan 08 was
protecting (install-fixture tests still pass).

Why dual-defmodule rather than inner-`if Code.ensure_loaded? do use Oban.Worker
... end`: Elixir 1.19 expands the `use` macro AST eagerly even inside `if false do
... end`, so any `use Oban.Worker` reference inside a defmodule body fails to
compile when Oban is absent — regardless of the if-condition or whether a module
attribute is used. The dual-defmodule form is the only shape that wraps the `use`
in a conditional that's actually evaluated before macro expansion.

Closes PR #37 CI Group B.
…ion to runtime via apply/3

Direct M.f/a calls to Assent.Strategy.OAuth2.refresh_access_token/2 in apple.ex,
facebook.ex, github.ex, and generic.ex resolve at compile time. In path-dep installs
where the host app does not declare Assent, `mix compile --warnings-as-errors` fails
with "Assent.Strategy.OAuth2.refresh_access_token/2 is undefined".

Convert to apply/3 so the call is resolved at runtime. Each refresh/3 function
already calls ensure_assent!() at the top, which raises if Assent is absent — so
runtime behaviour is preserved when Assent IS loaded, and continues to fail loudly
when it isn't.

Mirrors the Phase 93 plan 08 precedent in refresh_classifier.ex
(struct-pattern → map-pattern) for the function-call case.

Closes PR #37 CI Group D2.
…ps in smoke heredoc

Phase 92 / B2B-02 (Plan 92-01) made :roles a required option on
Sigra.Admin.Policy.admin_org_ids_from_memberships/2. The smoke script heredoc at
scripts/ci/admin-acceptance-smoke.sh:141 still called the helper with arity 1,
which surfaced as:

  Sigra.Admin.Policy.admin_org_ids_from_memberships/1 is undefined or private.
  Did you mean: admin_org_ids_from_memberships/2

during the generated-host compile. Match the example app's call shape
(test/example/lib/example/sigra_admin_policy.ex:38) and pass
roles: [:owner, :admin]. The smoke seeds an OrganizationMembership with
role: :admin; :owner is included to mirror the canonical example precedent.

Closes PR #37 CI Group D1.
… fixes

The Phase 11 golden fixture captured stdout output from an install where
sigra still emitted four `Assent.Strategy.OAuth2.refresh_access_token/2 is
undefined` warnings (one per OAuth strategy file). After commit 267033b
deferred those calls to runtime via apply/3, the warnings no longer fire,
so the fixture's first 28 lines were stale.

Regenerated via `MIX_ENV=test mix sigra.fixture.rebless_golden` (the
sanctioned re-bless tool). Delta:

  - 4 Assent OAuth2 undefined warnings removed (Group D2 — closed)
  - 1 typing violation added at lib/sigra/delivery.ex:48 — informational
    only, not fatal under --warnings-as-errors. Surfaced because the new
    `Sigra.Workers.EmailDelivery` stub branch (commit 611f48a) returns
    `no_return` when Oban is absent, which Elixir 1.19's type checker
    propagates back to `build_job/3`'s call site. Behaviour is unchanged
    (the unreachable-marker is gated by `OptionalDeps.ensure_available!`
    which raises before the marker is hit).

`mix sigra.fixture.rebless_golden --check` now reports "OK: fixture is
up-to-date." Local `mix test test/sigra/install/golden_diff_test.exs`
passes (2/2).
Quick task 260502-oc7 wrap-up. Three atomic fix commits + one fixture
rebless: workers now stay loadable when Oban absent (with tagged-error
stubs), Apple/Facebook/GitHub/Generic OAuth strategies defer Assent
resolution to runtime via apply/3, and admin smoke heredoc passes the
required :roles keyword. Local suite 2357/2358 passing — same UpgradeIntegrationTest
residual as the previous quick task, deferred to its own /gsd-debug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
example_unit_smoke, example_http_smoke, and example_playwright_smoke
all boot the example app via Example.Vault, which calls
System.fetch_env!("CLOAK_KEY") at init regardless of MIX_ENV. The
three jobs were missing the env var, causing System.EnvError at boot
and cascading failures across unit + HTTP + Playwright smoke lanes.

Three other jobs (mfa_e2e_playwright, oauth_e2e_playwright,
email_visual_regression) already have CLOAK_KEY at job-level; their
failures are unrelated to CLOAK_KEY and need separate diagnosis.

Test value is the deterministic 32-byte fixture used by sister jobs
(install_smoke, install_matrix); it is not a real secret.

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

Example.InstallCompileTest's setup_all overwrites the global
:persistent_term entry for ExampleWeb.Endpoint with a slim 4-key map
(host/url/static_url/struct_url) and never restores it. When the test
runs (i.e. when --include example_app is set in CI), every subsequent
test that calls Phoenix.ConnTest.dispatch/5 against the endpoint hits
KeyError on missing keys like :script_name, because the real
~30-key config map populated by Application.start has been replaced.

This was masked behind the CLOAK_KEY boot failure in earlier CI runs.
After commit 5f32cfc let the example app boot, this latent corruption
surfaced as 122 failures (out of 338) in Example unit smoke and 5
LiveView WebSocket attach failures in Example Playwright smoke.

Fix snapshots the original term in setup_all and restores it via
on_exit, so other test modules see the full Phoenix endpoint config.

Local: --include example_app drops from 127 failures to 19 (the
remaining 19 are unrelated pre-existing issues — login/OAuth/invitation
flows likely tied to Group F).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sigra.RateLimiters.Hammer.check_rate/3 raised when :hammer_module
config was unset, propagating a 500 from any plug pipeline that uses
Sigra.Plug.RateLimit. The auth_rate_limit pipeline added to generated
routers by default in 96-04 (commit 3accda8) wires through every
login / registration / confirmation request, so any host that hadn't
yet set up `config :sigra, hammer_module: <module>` saw 500s on every
auth flow.

Match the existing fail-open posture of the runtime branch (lines
44-48 already fail open when the configured module's GenServer is
unavailable). When :hammer_module is missing, log a one-time warning
with remediation copy and return `{:allow, ...}` so the request
proceeds without rate limiting.

Resolves the cascade behind PR #37 CI Group F (MFA backup-code E2E
and Generated admin Playwright registration/confirmation failures —
browser stayed on /users/confirm/ because the post-confirm POST
hit a 500 from this raise) and 19 local Example unit smoke failures
in SessionControllerTest, OAuthSettingsControllerTest, and
InvitationAcceptLiveTest.

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

The previous fix (4093a3c) tried to snapshot+restore the global
:persistent_term entry for ExampleWeb.Endpoint via on_exit, but
because the test module is `async: true`, other test modules running
concurrently in the same wave still saw the slim 4-key replacement
mid-run — the on_exit fires only when InstallCompileTest finishes,
which is too late for sibling modules already mid-flight. Result:
KeyError on :script_name / :path in 122+ Example unit smoke tests.

The :ets and :persistent_term shims are vestigial: none of this
module's actual tests (Code.ensure_loaded?, function_exported?, the
joken-warning compile capture) read endpoint config. Application.start
has already populated the real endpoint term with the full ~30-key
config map. Just delete the shims.

Local: --include example_app drops from 11 failures (with the snapshot/
restore fix) to 5 (with the deletion). The remaining 5 are unrelated:
1 pre-existing path-resolution test in InstallCompileTest itself, and
4 InvitationAcceptLiveTest failures that don't trace to this code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sigra.Organizations.Invitations.run_accept_multi/4 and
run_accept_with_signup_multi/4 compose a Multi that already contains
an :audit step from Sigra.Organizations.add_member_multi/5 (member_add
audit), then call append_audit/5 to add a second audit. Both used the
default :audit_multi_step name (:audit), causing
"#{step} is already a member of the Ecto.Multi" RuntimeError on every
invitation accept.

This was the root cause behind ExampleWeb.InvitationAcceptLiveTest's
4 happy-path failures (T9, T14, T17, T18) and likely a contributor to
PR #37 CI's MFA backup-code rotation E2E failure (which exercises the
invitation flow).

Mirror the established pattern from lib/sigra/api_token.ex,
lib/sigra/auth.ex, lib/sigra/mfa.ex, lib/sigra/service_accounts.ex —
extend invitations.ex's append_audit helper to forward
:audit_multi_step from extra opts, and pass a distinct
:accept_invitation_audit name from both run_accept_multi and
run_accept_with_signup_multi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Elixir's compile warnings emit paths relative to the runner's cwd.
When mix test runs in test/example/ (local default) the warning shows
`lib/example/accounts.ex`; when run from the project root (the
Example unit smoke CI invocation) it shows
`test/example/lib/example/accounts.ex`. The previous assertion
required the test/example/ prefix and failed in the local shape.

Match the trailing fragment that is stable across both cwds. The
warning still has to identify the source file — what we actually care
about — but the test no longer wedges on a cwd-dependent prefix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two surgical CI workflow fixes for PR #37 residuals (neither was a
real test/code regression):

1. MFA backup-code rotation E2E — Playwright spec passes; the job is
   red because "Generate MFA evidence report" runs in MIX_ENV=test
   but the rest of the job (compile + boot example app) runs in
   MIX_ENV=dev, so _build/test was never populated and mix bails
   with "the dependency is not available". Match the report step's
   env to the rest of the job (MIX_ENV=dev). The report task just
   emits markdown — env is cosmetic.

2. Email visual regression (GAUAT-01/02) — spec exits with
   "Project(s) email-chromium-{light,dark}, email-webkit-{light,dark}
   not found. Available projects: ... email-visual-chromium-light,
   email-visual-chromium-dark, email-visual-webkit-light,
   email-visual-webkit-dark". Project names in playwright.config.ts
   were prefixed with `email-visual-` at some point but the CI step
   wasn't updated to match. Use the actual project names.

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

Sigra.Plug.RateLimit.call/2 (rate_limit.ex:65) pattern-matches on
{:allow, %{remaining: ..., reset_ms: ...}}. The Noop fallback was
returning {:allow, 1} which crashed the plug with CaseClauseError on
every rate-limited request in any host that resolved to Noop.

Hosts hit Noop when:
- :limiter is not set in the plug opts (router default), AND
- Code.ensure_loaded?(Hammer) is false, i.e. the host app doesn't pull
  the optional :hammer hex package.

Phoenix 1.8 + mix sigra.install scaffolds a generated host that does
not pull :hammer (it's sigra's optional dep, not transitive through
path deps), so every fresh `mix sigra.install` hosts falls through to
Noop, then 500s on the first POST through the auth_rate_limit
pipeline (login, registration, etc.).

Resolves the Generated admin Playwright smoke + Example Playwright
smoke failures behind PR #37 — both saw `expect(page).not.toHaveURL`
fail because the form POST hit a 500 from this CaseClauseError and
the browser stayed on /users/log_in.

Match Hammer's fail-open shape: {:allow, %{count, remaining, reset_ms}}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. MFA backup-code rotation E2E — Generate MFA evidence report step
   was running mix from the repo root, but the library's deps.get had
   not been run in this job (only the example app's), so mix bailed
   with "the dependency is not available" even after the previous
   MIX_ENV=test→dev fix. Run from test/example/ where sigra is a
   path dep with deps already fetched.

2. Email visual regression — when the L2 Playwright spec fails, the
   test-results/ subdirectory contains the actual.png + expected.png
   + diff.png triple needed to triage real regression vs cwd-stable
   drift. The existing bundle assemble step is gated behind L3
   reports (skipped on L2 fail), so artifacts were never uploaded.
   Add a dedicated failure-path upload of test-results/ +
   playwright-report/ so snapshot diffs are inspectable from CI.

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

The committed baselines were generated against a different chromium /
font-rendering version than the one CI runs today. All 36 email-visual
PNGs (9 templates × 2 engines × 2 themes) and the single
oauth-link-disabled-tooltip baseline diff cleanly when CI renders
them — visual content is identical (Account Locked layout, copy,
links, button) but sub-pixel positioning and antialiasing differ
enough to trip the pixel-diff threshold.

Replace each baseline with the corresponding actual.png pulled from
run 25277800584's email-visual-failure-diagnostics and
oauth-e2e-playwright-failure-diagnostics artifacts (CI Linux
renders, post-Noop-fix so no transient 5xx pages baked in).

Local Mac chromium != CI Linux chromium, so a `--update-snapshots`
flow run on a developer machine would re-introduce the drift on the
next CI push. Future rebaselines should pull from CI artifacts
the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Explains where Sigra sits relative to Lockspire (OAuth/OIDC
provider) and Relyra (SAML SP) with a wiring diagram, role
table, and adoption decision tree. Companion readers landing
on Sigra's hexdocs need this map before reaching for the
right companion library. Surfaced near the top of the
Introduction section, just after installation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces fictional Sigra.Identity.find_or_create_from_provider/2 and
Sigra.Session.get_or_create/4 references in the Relyra↔Sigra wiring
steps with the real APIs (host-owned upsert + Sigra.Auth.create_session/3
+ Plug.Conn.put_session/3). Also updates the AccountResolver callback
count from "five" to "six" — resolve_account/2 (introspection/refresh
lookup) was missing from the prose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes HARD-01, HARD-03, and API-01 from Pending to Complete in
REQUIREMENTS.md and ROADMAP.md. All seven v1.21 requirements have
passing-test evidence (substantive 7/7) and the v1.21 milestone audit
on 2026-05-06 closed the strict 3-source matrix gaps via:

- 91-0[1-7]-SUMMARY: add `requirements-completed: [B2B-01]`
- 94-0[1-4]-SUMMARY: add `requirements-completed: [HARD-01]`
  (94-04 prepended full frontmatter; was plain markdown)
- 96-0[1-4]-SUMMARY: prepend frontmatter with HARD-03 / API-01 /
  [HARD-03, API-01] mapping per plan content
- 94-VERIFICATION: add YAML frontmatter (status: passed); closes the
  stale environmental Oban-test caveat — `MIX_ENV=test mix compile
  --warnings-as-errors` now exits 0 and golden_diff_test runs 2/2
- 96-VERIFICATION: add YAML frontmatter (status: passed) with the
  4 evidence-section test counts
- ROADMAP.md: Phase 93/94/96 boxes promoted to [x]
- STATE.md: refresh focus to milestone-close (was stale Phase 93)

The new .planning/v1.21-MILESTONE-AUDIT.md captures the audit verdict
(tech_debt → reconciled) along with cross-phase integration findings,
remaining tech debt (2 install-smoke todos, DEF-92-02-01 pre-existing
bug), and Nyquist coverage status (only Phase 95 nyquist_compliant:
true; the others are draft or missing).

Open at milestone close (non-blocking):
- 2 install-smoke pending todos from 2026-04-30
- DEF-92-02-01 audit Multi step-name collision (predates Phase 92)
- Nyquist VALIDATION.md gaps for 91/92/93/94/96 (optional retro fill)

Next: /gsd-complete-milestone v1.21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Archives the v1.21 B2B-ready & production-honest milestone (Phases 91-96,
shipped 2026-05-06) per the standard close-out flow.

What shipped:
- B2B-01 (Phase 91): Org-level MFA enforcement — Sigra.Plug.RequireOrgMfa,
  enforce_mfa_for_members, atomic organization.mfa_policy_change audit row
- B2B-02 (Phase 92): RBAC seams — Sigra.Authz behaviour, nullable role on
  memberships, scope-struct :role propagation, RBAC recipe (zero opinionated
  roles in lib/sigra/)
- B2B-03 (Phase 93): M2M service-account tokens — client_credentials grant
  on existing JWT path, current_scope.actor_type :service_account
  discriminator, 5/5 SA mutations co-fated with audit (re-verified 22/22)
- HARD-01 (Phase 94): Postgres-only declaration — pre-flight refusal,
  removed MySQL/SQLite placeholder branches; environmental Oban-test
  caveat closed in 2026-05-06 audit
- HARD-02 (Phase 95): Optional-dep boot validation — Sigra.OptionalDeps SOT,
  raise-on-missing for Oban/Bcrypt/EQRCode, mix sigra.doctor, 3 dep-off CI
  lanes (only v1.21 phase with nyquist_compliant: true)
- HARD-03 + API-01 (Phase 96): OAuth refresh dispatch + rate-limit headers
  — 122 passing tests across 4 evidence sections

Bookkeeping changes:
- Created .planning/milestones/v1.21-ROADMAP.md (full archive)
- Created .planning/milestones/v1.21-REQUIREMENTS.md (all reqs Complete)
- Moved .planning/v1.21-MILESTONE-AUDIT.md → .planning/milestones/
- Updated PROJECT.md: v1.21 promoted to "Previously closed milestones";
  Current Milestone is now between-milestones with /gsd-new-milestone
  pointer
- Updated MILESTONES.md: appended v1.21 entry (scope, accomplishments,
  stats, deferred items, archive links)
- Updated RETROSPECTIVE.md: appended v1.21 retrospective section (what
  was built, what worked, what was inefficient, patterns established,
  key lessons)
- Collapsed v1.21 in ROADMAP.md to one-line summary; removed phase details
  block; kept Backlog
- Removed REQUIREMENTS.md (archived to milestones/; fresh one will be
  created by /gsd-new-milestone for the next cycle)
- Updated STATE.md to between-milestones state

Open at close (non-blocking, carried forward):
- 2 install-smoke pending todos from 2026-04-30 (JOSE warning, transient
  Postgres too_many_connections)
- DEF-92-02-01 — pre-existing audit Multi step-name collision (predates
  Phase 92)
- Nyquist VALIDATION.md gaps — only Phase 95 has nyquist_compliant: true;
  91/92/93 draft, 94/96 missing. Optional retro fill via /gsd-validate-phase.

See .planning/milestones/v1.21-MILESTONE-AUDIT.md for the audit verdict
(tech_debt → reconciled).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI-red fixes for PR #37:

1. test/sigra/behaviours_test.exs — Noop.check_rate/3 contract test
   was still asserting {:allow, 1} after b05b9e8 widened the return
   shape to {:allow, %{count, remaining, reset_ms}} to match what
   Sigra.Plug.RateLimit reads. Test now pattern-matches the new
   shape with stable count/remaining and a positive integer reset_ms
   guard so timing variation doesn't introduce flake.

2. .github/workflows/ci.yml — OAuth E2E "Generate OAuth evidence
   reports" step was running mix from the repo root in MIX_ENV=test,
   but the library deps are not fetched in this job (only the example
   app's deps under test/example/ in MIX_ENV=dev). Mix bailed with
   "the dependency is not available, run mix deps.get" for the entire
   library dep set. Mirrors the MFA evidence-report fix in 2974be6:
   add `working-directory: test/example` and switch MIX_ENV to dev so
   sigra-as-path-dep resolves cleanly.

Verified locally: MIX_ENV=test mix compile --warnings-as-errors
exits 0; the noop limiter test now passes against the updated
contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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