Skip to content

Tier 1 feature gap improvements (all 9 features)#6

Merged
Ch4s3 merged 48 commits into
mainfrom
claude/friendly-sinoussi-bf98ba
May 23, 2026
Merged

Tier 1 feature gap improvements (all 9 features)#6
Ch4s3 merged 48 commits into
mainfrom
claude/friendly-sinoussi-bf98ba

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Implements all nine Tier 1 features from the feature-gap analysis, test-first, across the four sequenced plans in docs/superpowers/plans/2026-05-21-tier1-0{1..4}-*.md. No breaking changes — existing gates serialize with value: nil and behave exactly as before.

Plan 1 — Quick Wins

  • Per-call :default fallback on enabled?/2 (fail-open on store errors)
  • Opt-in Bandera.Audit log: Event struct + attach/2/detach/1 over existing write telemetry, with a :by identity option threaded into enable/disable/clear

Plan 2 — Schema v2 + Multivariate (foundation for 3 & 4)

  • Nullable value column (JSON via Jason) + upgrade_v2/0 migration helper
  • :variant gates with stable per-actor bucketing; Bandera.variant/2 + put_variants/3
  • Full adapter parity (Ecto, Redis, Memory/ProcessScoped)

Plan 3 — Data-Defined Targeting

  • Bandera.Constraint with 10 operators + map round-trip
  • :rule gates (constraint list in value), context-aware Flag.enabled?/2, enable(when:), :context option
  • Reusable named segments: put_segment/2, :segment gates expanded at evaluation time in the impure layer (Flag stays pure)

Plan 4 — Prerequisites, Scheduling & Tooling

  • :prerequisite gates via requires: with cycle detection
  • :schedule gates with pure UTC time-window evaluation (active in both actor and non-actor paths)
  • Stale-flag tooling: Bandera.Usage ETS/telemetry tracker, stale_flags/1, mix bandera.flags --stale

Each plan went through spec-compliance and code-quality review plus a final cross-plan review; fixes are committed (notably: variant all-zero-weight guard, SQLite-safe upgrade_v2/0, percentage-of-time double-draw fix, lenient regex/schedule parsing that fails closed, and Usage tracking variant/2 evaluations).

Test Plan

  • mix test — 339 passed (81 doctests, 8 properties, 250 tests), 13 Redis integration tests excluded (no local Redis)
  • mix format --check-formatted — clean
  • mix credo --strict — no issues
  • mix assay (incremental Dialyzer) — 0 errors
  • CI runs the Redis-backed suite (the 13 locally-excluded tests) at ~91% coverage

Ch4s3 added 30 commits May 22, 2026 16:23
- from_telemetry/2: fallback clause returns :ignore for unexpected events;
  attach/2 callback skips :ignore so a future event-list change can't crash
- audit_test: on_exit detach guard prevents handler leak on assertion failure
- document :by option on enable/2, disable/2, clear/2
- fix from_telemetry arity in test description (/3 -> /2)
- Gate.new(:variant) rejects all-zero weight maps (was silently returning the
  alphabetically-last variant); add a regression test
- Migrations.upgrade_v2/0 uses plain add/2 instead of add_if_not_exists/2, which
  the SQLite3 adapter rejects ("Not supported by SQLite3"); add a migrator-driven
  test that proves the column is added to a v1 table
- fix Bandera.variant/2 docstring (flag name, not actor name); drop dead code in
  the schema-v2 column test
- Flag.enabled?/2 actor path: resolve percentage only via check_percentage_gate/3
  instead of folding base/1 in, so a percentage_of_time gate is no longer drawn
  twice (which inflated its effective probability); restores pre-Plan-3 semantics
- Constraint :matches compiles patterns leniently (Regex.compile/1) so a malformed
  stored pattern is a non-match rather than crashing every evaluating caller;
  add a regression test
- document the intentional grant-when-matched (always-enabled) encoding for rule
  gates in the Redis serializer
- clarify that an explicit empty context equals no context
Ch4s3 added 18 commits May 22, 2026 17:31
- schedule gate fails closed on a malformed/non-ISO-8601 window bound instead of
  crashing every evaluation (DateTime.compare on a non-DateTime); add a test
- stale_flags/1 excludes internal bandera_segment:* keys, which are never
  evaluated and so would always look stale; add a test
- mix bandera.flags --stale warns when Bandera.Usage isn't running
- cover the requires: {parent, false} (parent-must-be-disabled) path
- Bandera.Usage tracks [:bandera, :variant] as well as [:bandera, :enabled?] so
  flags resolved only via variant/2 are not misreported as stale; add a test
- mix bandera.flags listing also hides internal bandera_segment:* flags, matching
  stale_flags/1
- fix README upgrade_v2/0 description to match the plain add/2 implementation
- variant weight distribution (deterministic) + zero-weight never chosen
- schedule gate boundaries: open-ended, from-only, until-only
- multi-level prerequisite chain and prerequisite x context-rule interaction
- :default fallback through the :context path on store error
- rule and schedule gates end-to-end through the Ecto value column
- Bandera.Usage records via a real enabled?/2 call
…e reasons to use Bandera

- reframe Why Bandera? around the three advanced capabilities (multivariate flags,
  data-defined targeting, scheduling), keeping runtime config and async testing as
  the fundamentals
- expand the intro to mention variants, rules/segments, prerequisites, scheduling
- add Prerequisites and Scheduling sections (previously only in the feature guide)
- note schedule gates in the evaluation-precedence summary
- link the new Feature Guide and the other guides from the docs section
- coerce numeric strings before eq/neq/in/not_in and ordering, so a string context
  value (the common JSON/params case) is compared by value, not by Elixir term
  ordering. Fixes silent gate inversion: "5" no longer passes >= 18, a stringified
  banned id is still caught by :not_in, etc. Number/non-numeric-string ordering now
  fails closed.
- memoize compiled :matches regexes in :persistent_term instead of recompiling on
  every evaluation; document that :matches is unanchored

Addresses adversarial findings #1 (type confusion) and #6 (regex recompile).
A negative weight passed the previous "at least one positive" guard and made the
total <= 0, which silently routed 100% of actors to the alphabetically-last variant.
Validate that every weight is a non-negative number. Addresses adversarial finding #3.
enable(when: []) and put_segment(name, []) now raise ArgumentError, and
Flag.enabled? ignores a rule gate with no constraints (Enum.all?([]) is vacuously
true, which previously granted to every context). Addresses adversarial finding #9.
…nown parents

- memoize each flag's resolved state within a single enabled?/2 call, so a shared
  (diamond) prerequisite graph is evaluated once instead of O(2^depth) re-walks
- propagate cycles as a distinct :cycle status so they fail closed uniformly,
  including required:false edges (which a plain false previously satisfied, making
  a mutual requires:{_,false} cycle report both flags enabled)
- treat a non-atom (unresolved) parent name as not-met (fail closed)

Addresses adversarial findings #5, #7, and the prerequisite half of #4.
- a corrupt or foreign gate (bad JSON, unknown gate_type/field, malformed ratio)
  is now dropped with a warning instead of raising, so one bad row no longer takes
  down deserialize_flag / all_flags / the dashboard
- prerequisite parent names resolve via String.to_existing_atom (kept as a string,
  which fails closed, when the atom is unknown) so corrupt store data can't exhaust
  the atom table

Addresses adversarial findings #2, #L7, and the serializer half of #4.
Telemetry auto-detaches a handler that raises, which would silently stop the audit
log (a compliance gap) or usage tracking. Catch and log exceptions from the user's
audit callback, and guard the usage handler, so a transient failure no longer kills
the hook. Addresses adversarial finding #8.
- stale_flags/1 clamps :older_than to >= 0 so a negative value can't push the cutoff
  into the future and report every flag (incl. fresh ones) as stale (#L2)
- document the new enable/2 scopes and clarify that conditional scopes (when/
  for_segment/requires/schedule) return {:ok, true} on a successful write and are
  evaluated per call (#L9)
@Ch4s3
Ch4s3 merged commit c65c6fc into main May 23, 2026
2 checks passed
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