v1.21 batch: Phases 91-96 verified (B2B-01..03, HARD-01..02, HARD-03/API-01)#37
Open
v1.21 batch: Phases 91-96 verified (B2B-01..03, HARD-01..02, HARD-03/API-01)#37
Conversation
- 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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Branch
chore/phase-88-uat-evidencehas 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 ✓
oauth.token_refreshedaudit co-fate.X-RateLimit-Limit / -Remaining / -ResetandRetry-Afterheaders from Hammer state.rg "not yet implemented" lib/sigra/oauth.ex→ no matches).Phase 95 — Optional-dep boot validation +
mix sigra.doctor(HARD-02) — verified ✓mix sigra.doctorreports per-feature dep status..github/workflows/ci.yml.Phase 94 — Postgres-only declaration (HARD-01) — verified ✓
mix sigra.installrefuses to run against a non-Postgres adapter with a clear error.README.md,guides/introduction/{getting-started,installation}.md,.planning/PROJECT.md, andmix.exsdescription all state PostgreSQL as the only supported adapter.Phase 93 — M2M / service-account tokens (B2B-03) — verified ✓ (22/22, re-verified 2026-05-02)
client_credentialsgrant on the existing JWT path,current_scope.actor_typedistinction, audit rows distinguishable from user-tied tokens, generated host wiring for issue / list / revoke.Repo.transact/2so a failed audit aborts the mutation).FetchBearerparity tests covering scope-build for SA tokens vs. user tokens.UI-SPECparity forOrganizationServiceAccountsLive(template +:showroute + example-app mirror +CopyToClipboardhook source + asset injection + installer wiring).gaps_found(18/22, 3 open gaps). All three gaps closed:Postgrex.Errorstruct match (lib/sigra/service_accounts.ex:229,414),nil-and BadBooleanErrorfix inorganization_service_accounts_live.ex:488,650, CHANGELOG[Unreleased]carries the B2B-03 trace bullet.MIX_ENV=dev mix compile --warnings-as-errorsexits 0; E2E test passes.Phase 92 — RBAC seams (B2B-02) — verified ✓
Sigra.Authzbehaviour (singlecan?/3callback, no built-in policy).lib/sigra/admin/policy.ex,lib/sigra/organizations.ex,lib/sigra/plug/require_membership.ex,lib/sigra/organizations/invitations.exconsume host-supplied roles only.<App>.SigraAuthzallow-all starter; nullablerole :stringmigrations;Sigra.Ecto.Types.RoleAtomfor atom round-trip; dead:require_org_ownerpipeline removed.current_scope.rolepopulated frommembership.rolethroughSigra.Scope.HydrationandSigra.Plug.PutActiveOrganization. Plug ↔ on_mount parity via shared hydrator. Nil-user / stale / no-org branches leave role nil.guides/recipes/role-based-access-control.md, 252 lines) registered with ExDoc, walks adopters allow-all → deny-by-default.Phase 91 — Org-level MFA enforcement (B2B-01) — verified ✓
organization.mfa_policy_changeaudit row.Sigra.Audit.log_multi_safe/3fromSigra.Organizations.set_mfa_policy/5— no newlog_safe/3debt.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
.planning/phases/91-org-level-mfa-enforcement-b2b-01/91-VERIFICATION.md.planning/phases/92-rbac-seams-b2b-02/92-VERIFICATION.md.planning/phases/93-m2m-service-account-tokens-b2b-03/93-VERIFICATION.mdOban.Workercompilation under Elixir 1.19.5, pre-existing onmain) — see.planning/phases/94-postgres-only-declaration-hard-01/94-VERIFICATION.md.planning/phases/95-optional-dep-boot-validation-mix-sigra-doctor-hard-02/95-VERIFICATION.md.planning/phases/96-oauth-refresh-rate-limit-headers-hard-03-api-01/96-VERIFICATION.mdKey decisions
rolesandowner_rolearerequired: trueconfig options; the library raisesKeyErrorwith an actionable message if a host calls into an RBAC-aware function without supplying them.[:owner, :admin, :member]for DX, but framed as edit-to-customize.can?/3is the SC1-mandated default starter (Phase 92). The recipe shows the deny-by-default hardening path.:rolewrite seam (Phase 92).Sigra.Plug.PutActiveOrganizationwritesscope.roleafter the host scope_module callback.actor_typedistinguishes service-account vs. user tokens (Phase 93) — same JWT path, different audit shape, different revoke semantics.Repo.transact/2.mix sigra.doctoris the diagnostic surface.invalid_grantclassification (Phase 96).oauth.token_refreshedaudit co-fated with token rotation.DEF-92-02-01—InvitationAcceptLiveMulti step-name collision, pre-existing bug from 2026-04-15. Tracked for a follow-up phase.93-VERIFICATION.md.Test plan
mix sigra.installagainst a fresh host produces the expectedSigraAuthzmodule + nullable role column + service-account scaffolding + Postgres-only refusal on a non-Postgres adaptercurrent_scope.roleandcurrent_scope.actor_typepopulate correctly in a generated host (manual smoke)mix sigra.doctoroutput reports per-feature dep status correctly with each optional dep toggled offPOST /users/log_inup to the limit and that the 11th request 429s with all headers present🤖 Generated with Claude Code