{"id":47705191,"url":"https://github.com/yplog/gorege","last_synced_at":"2026-04-25T13:13:34.695Z","repository":{"id":347578146,"uuid":"1194420821","full_name":"yplog/gorege","owner":"yplog","description":"Zero-dependency Go library for first-match rule evaluation — access control, feature flags, A/B testing, and more.","archived":false,"fork":false,"pushed_at":"2026-04-25T11:40:50.000Z","size":394,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T12:22:25.681Z","etag":null,"topics":["access-control","decision-engine","feature-flags","go","golang","rule-engine"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/yplog/gorege","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yplog.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-28T10:34:03.000Z","updated_at":"2026-04-25T11:39:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yplog/gorege","commit_stats":null,"previous_names":["yplog/gorege"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/yplog/gorege","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yplog%2Fgorege","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yplog%2Fgorege/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yplog%2Fgorege/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yplog%2Fgorege/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yplog","download_url":"https://codeload.github.com/yplog/gorege/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yplog%2Fgorege/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32263038,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T09:15:33.318Z","status":"ssl_error","status_checked_at":"2026-04-25T09:15:31.997Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-control","decision-engine","feature-flags","go","golang","rule-engine"],"created_at":"2026-04-02T17:53:40.032Z","updated_at":"2026-04-25T13:13:34.672Z","avatar_url":"https://github.com/yplog.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/logo.png\" alt=\"gorege\" width=\"400\"\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/yplog/gorege/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/yplog/gorege/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/yplog/gorege\"\u003e\u003cimg src=\"https://codecov.io/gh/yplog/gorege/branch/main/graph/badge.svg\" alt=\"Coverage\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/yplog/gorege\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/yplog/gorege?nocache=true\" alt=\"Go Report Card\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pkg.go.dev/github.com/yplog/gorege\"\u003e\u003cimg src=\"https://pkg.go.dev/badge/github.com/yplog/gorege.svg\" alt=\"Go Reference\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"License: MIT\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n# gorege\n\nA small Go library for **first-match rule evaluation** over a fixed tuple of dimensions: access control, feature flags, A/B cohorts, product availability, and similar decisions all map to the same pattern.\n\nDesign goals: idiomatic Go, immutable engines safe for concurrent use, explicit semantics (including `Explain` and dead/shadow rule warnings), and a true BFS-based `Closest` search for minimum Hamming distance. The API is influenced by [recht](https://github.com/dashersw/recht); gorege adds stronger guarantees and observability.\n\n- **Go 1.26+**\n- **Zero runtime dependencies** (standard library only)\n- **JSON** configuration via `Load` / `LoadWithOptions` / `LoadFileWithOptions` (`.json` only)\n\n## Install\n\n```bash\ngo get github.com/yplog/gorege\n```\n\n## Quick start\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/yplog/gorege\"\n)\n\nfunc main() {\n\te, warnings, err := gorege.New(\n\t\tgorege.WithDimensions(\n\t\t\tgorege.Dim(\"membership\", \"Gold member\", \"Regular member\", \"Guest\"),\n\t\t\tgorege.Dim(\"day\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\"),\n\t\t\tgorege.Dim(\"facility\", \"Swimming pool\", \"Gym\", \"Sauna\"),\n\t\t),\n\t\tgorege.WithRules(\n\t\t\tgorege.Allow(\"Gold member\", gorege.Wildcard, gorege.Wildcard),\n\t\t\tgorege.Deny(\"Guest\", gorege.AnyOf(\"Mon\", \"Tue\"), \"Sauna\"),\n\t\t\tgorege.Allow(gorege.AnyOf(\"Guest\", \"Regular member\"), gorege.Wildcard, gorege.Wildcard),\n\t\t),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfor _, w := range warnings {\n\t\tlog.Printf(\"rule warning: %s\", w)\n\t}\n\n\tok, err := e.Check(\"Guest\", \"Mon\", \"Sauna\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Println(ok) // false\n\n\tok, err = e.Check(\"Guest\", \"Wed\", \"Sauna\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Println(ok) // true\n}\n```\n\n### Rule shape\n\nEach rule is **ALLOW** or **DENY** plus one matcher per dimension (in order):\n\n| Matcher in code | Meaning |\n|-----------------|--------|\n| `\"exact\"` string | Exact value |\n| `gorege.AnyOf(\"a\", \"b\")` | Any listed value |\n| `gorege.Wildcard` | Any value **declared** for that dimension |\n\nEvaluation is **first match wins**. If nothing matches, `Check` returns `false`. Shorter rules implicitly wildcard trailing dimensions.\n\n`Check` requires exactly as many arguments as dimensions (`ErrArityMismatch` otherwise). `PartialCheck` allows a prefix tuple, including **zero** values (empty prefix: “could any full tuple still be allowed?”), with Recht-style trailing “unconstrained” behaviour. It returns `(bool, error)`; if you pass **more** values than dimensions you get `ErrArityMismatch` instead of a bare `false`, so overload is not mistaken for denial.\n\n## JSON config\n\n`LoadFileWithOptions`, `Load`, and `LoadWithOptions` decode the same schema (call `LoadFileWithOptions(path)` with no extra options for a plain file load). Extra options (for example `WithAnalysisLimit`) apply after the JSON-derived dimensions and rules. Example (see also `testdata/rules.json`):\n\n```json\n{\n  \"dimensions\": [\n    { \"name\": \"membership\", \"values\": [\"Gold member\", \"Regular member\", \"Guest\"] },\n    { \"name\": \"day\", \"values\": [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\"] },\n    { \"name\": \"facility\", \"values\": [\"Swimming pool\", \"Gym\", \"Sauna\"] }\n  ],\n  \"rules\": [\n    { \"action\": \"ALLOW\", \"name\": \"allow-gold\", \"conditions\": [\"Gold member\", \"*\", \"*\"] },\n    { \"action\": \"DENY\", \"name\": \"deny-guest-sauna-early-week\", \"conditions\": [\"Guest\", [\"Mon\", \"Tue\"], \"Sauna\"] },\n    { \"action\": \"ALLOW\", \"name\": \"allow-rest\", \"conditions\": [[\"Guest\", \"Regular member\"], \"*\", \"*\"] }\n  ]\n}\n```\n\n- `\"*\"` in JSON is a wildcard (same as `gorege.Wildcard`).\n- A JSON array of strings in a slot is `AnyOf`.\n- Omit `name` on a dimension to get an anonymous axis (`DimValues`-style).\n\nOn `New`, `Load`, `LoadWithOptions`, or `LoadFileWithOptions`, the engine reports **warnings** for rules that never match any tuple in the Cartesian product (“dead”) or never win first-match (“shadowed”), unless analysis is skipped (see below). Dead detection does not enumerate the product; shadow detection does, subject to a tuple cap. Each `Warning` includes `Kind` (`WarningKindDead`, `WarningKindShadowed`, or `WarningKindAnalysisLimitExceeded`) so callers need not parse `Message`.\n\n\u003e **Performance note:** Shadowed-rule analysis walks the Cartesian product of declared dimension values. With large dimension sets (e.g. 6 dimensions × 20 values = 64 000 000 tuples) this can be slow. The default cap is 100 000 tuples for that pass; use `WithAnalysisLimit(n)` with `New` or `LoadWithOptions` / `LoadFileWithOptions` to adjust, or pass a negative value to skip analysis entirely. When the cap is exceeded, dead rules are still reported.\n\n## Bring Your Own Parser\n\n`gorege.Config`, `gorege.DimensionConfig`, and `gorege.RuleConfig` are exported. To build an engine from YAML, TOML, or another format, decode into these types with your own parser, then call `NewFromConfig`:\n\n```go\nimport \"gopkg.in/yaml.v3\" // in your project; not a gorege dependency\n\nvar cfg gorege.Config\nif err := yaml.Unmarshal(data, \u0026cfg); err != nil { ... }\n\ne, warnings, err := gorege.NewFromConfig(cfg,\n    gorege.WithAnalysisLimit(50_000),\n)\n```\n\n`gorege` uses only `encoding/json` internally. The `yaml:\"...\"` struct tags let you unmarshal with any YAML library on your side while keeping gorege a zero third-party dependency as a library.\n\n## API overview\n\n| Area | Functions |\n|------|-----------|\n| Build | `New`, `NewFromConfig`, `WithDimensions`, `WithRules`, `WithTiebreak`, `WithAnalysisLimit` (shadow analysis tuple cap, default 100 000) |\n| Inspect | `Dimensions`, `Rules` (defensive copies) |\n| Evaluate | `Check`, `PartialCheck`, `Explain` |\n| Nearest allow | `Closest` — BFS by Hamming distance from the input; **any** dimensions may change until an allowed tuple is found. `ClosestIn` — **only** the selected dimension changes (others fixed); `dim` is an index or dimension name. Tiebreak (`WithTiebreak`): leftmost / rightmost / declaration order affects `Closest` search and reporting. |\n| Config | `LoadFileWithOptions`, `Load`, `LoadWithOptions` (`.json` only) |\n| Types | `Dimension`, `Rule`, `Action`, `Explanation`, `ClosestResult`, `Warning`, `WarningKind`, `Config`, `DimensionConfig`, `RuleConfig` |\n\n`Engine` is immutable and safe to share. For hot reload, load a new engine and swap a `sync/atomic.Pointer` holding `*gorege.Engine`.\n\n## CLI\n\n```bash\ngo install github.com/yplog/gorege/cmd/gorege@latest\n# or: task build-cli  → ./bin/gorege\n\ngorege check path/to/rules.json Guest Wed Sauna   # prints true/false; exit 1 if denied or error\ngorege partial-check path/to/rules.json Guest     # prefix [Engine.PartialCheck]: 0..N values (N = #dims); true if some completion could still be allowed\ngorege explain path/to/rules.json Guest Wed Sauna # which rule matched (debug); exit 1 on load/arity error only\ngorege closest path/to/rules.json Guest Wed Sauna # nearest allowed tuple (BFS); exit 1 if none exists\ngorege closest-in path/to/rules.json 2 Guest Wed Sauna   # same, varying only dim index 2\ngorege closest-in path/to/rules.json facility Guest Wed Sauna # or dimension name\ngorege lint path/to/rules.json                    # dead/shadow warnings (or \"ok\"); exit 1 if any warnings\n```\n\n**Where loader warnings go:** For `check`, `explain`, `partial-check`, `closest`, and `closest-in`, the main result is on **stdout** (for example `true`/`false` or `explain` fields), so engine load warnings (dead rules, shadowed rules, analysis limit, …) are printed to **stderr** as secondary output. **`lint`** is the opposite: those warnings *are* the intended output, so each message is printed to **stdout** (or `ok` when there are none), which keeps `lint` easy to pipe or scrape; load errors still go to **stderr**.\n\n`explain` prints `matched`, `allowed`, `rule_index`, `rule_name`, and `action` (or a line for implicit deny when no rule matches). Exit code stays `0` when the explanation was computed successfully.\n\n`closest` walks increasing Hamming distance and may change several dimensions at once (`Engine.Closest`). `closest-in` only tries alternate values on one axis (`Engine.ClosestIn`). Both print `found`, `conditions` (JSON array), `distance` (Hamming distance from the input tuple), `dim_index`, `dim_name`, and `value` for the reported pivot dimension. `found: false` uses exit code `1`. For `closest-in`, a numeric-only selector is treated as a dimension index; otherwise it is resolved as a name (same as the library).\n\n## Examples\n\nThe [`examples/`](./examples) directory contains self-contained runnable\nprograms demonstrating real-world usage patterns.\n\n| Example | Scenario | API surface |\n|---------|----------|-------------|\n| [`feature_flags/`](./examples/feature_flags) | Feature gate by plan x region | `LoadFileWithOptions`, `Check`, `PartialCheck`, `ClosestIn` |\n| [`ecommerce_availability/`](./examples/ecommerce_availability) | Product variant availability by region x tier x channel x category | `Check`, `Explain`, `PartialCheck`, `Closest`, `ClosestIn`, hot reload via `atomic.Pointer` |\n\n```bash\ncd examples/feature_flags \u0026\u0026 go run . rules.json\ncd examples/ecommerce_availability \u0026\u0026 go run . rules.json\n```\n\n## Development\n\nThis repo uses **[mise](https://mise.jdx.dev/)** for pinned Go (see `mise.toml`) and **[Task](https://taskfile.dev/)** for common commands:\n\n| Task | Purpose |\n|------|---------|\n| `task` / `task test` | Unit tests |\n| `task cover` | Coverage (profile + merged summary line) |\n| `task build-cli` | Build `bin/gorege` |\n| `task ci` | `gofmt`, `vet`, `test`, `build` |\n| `task fuzz-load` / `task fuzz-check` | Go fuzz (default 5s; e.g. `task fuzz-load FUZZTIME=30s`) |\n\nFuzz targets live in `fuzz_test.go`. Normal `go test` runs each fuzz function once with its seed corpus; use `-fuzz=FuzzLoad` (etc.) for real fuzzing.\n\n## Layout\n\n```\ngorege.go    Engine, New, options\ntrie.go      Priority Multi-path Trie (always active when dims and rules are present)\nrule.go      Rules, matchers, Allow/Deny\ndimension.go Dimensions\ncheck.go     Check, PartialCheck, Explain\nclosest.go   Closest, ClosestIn, tiebreak\nconflict.go  Dead / shadow warnings\nloader.go    Config types, NewFromConfig, JSON Load / LoadFileWithOptions\nresult.go    Explanation, ClosestResult, Action helpers\ncmd/gorege   CLI\nfuzz_test.go Go fuzz targets (Load, Check, …)\ntestdata/    Example JSON fixtures\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyplog%2Fgorege","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyplog%2Fgorege","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyplog%2Fgorege/lists"}