Every answer explains itself.
Tierwise is a type checker and evaluator for layered business configuration. You declare your own levels, keys and decisions. A merge rule on every key, explicit all or any over collections, and provenance on every answer make the result mean one thing, the same thing, in Java and in JavaScript.
git clone https://github.com/djkeshawa/visp-tierwise
Pre-release. Engineering is complete and hosted CI is green, but 0.1.0 isn’t on Maven Central or npm yet. Build from source today.
checkout.pickup.lead_hours
division: south · seller: acme
- global2set
- divisionsouth · 6, the most specific level on the pathset
- selleracme · 8, an axis across the levelssource
- ruleoverride, then
combine_across_axes: maxbetween the levels and the seller - bundlesha256:ab1cd3a9… · the same bytes give the same answer in Java and in Node.
- 1Declare
tierwise.yamlLevels, keys with a merge rule each, decisionsyour repo - 2Validate
tierwise validateTypes, typos, admission and lint, with suggestionsfails CI - 3Test
tierwise testYour examples, each with an expected effectfails CI - 4Compile
tierwise compileCanonical bundle, named by its SHA-256bundle.json - 5Evaluate
resolve · decideJava or Node, in process; browsers for displayyour services
Merge rules
One rule per key, said out loud.
Each key declares how values from different levels combine. Choose a rule to see three levels resolve to one answer.
- global
2 - division
6 - site
4most specific
The most specific value on the path wins. Needs a default or a root value.
- global
{header: "Acme", footer: "Thanks"} - division
{footer: "Merci"} - site
{show_logo: true}
{header: "Acme", footer: "Merci", show_logo: true}. Fields merge recursively; lists are replaced.
- global
true - division
falseveto - site
true
AND across the path: one false anywhere vetoes it. A capability, so lower levels can only narrow.
- global
false - division
false - site
truegrants
OR across the path: one true turns it on.
- global
[GROUND, EXPRESS, PICKUP] - division
[GROUND, PICKUP]narrows - site
[PICKUP, EXPRESS]
The declared universe, intersected with every value on the path.
- plan
[csv] - org
[pdf] - workspace
—
Every grant on the path adds to the set.
- global
500 - division
100smallest - site
250
The smallest bound wins, so a site can’t raise a division’s limit.
- global
10 - division
25largest - site
—
The largest value wins. If nothing on the path sets it, the result is absent.
override and deep_merge depend on order and need one. There is no null, no coercion and no float.Decisions
“All” or “any”, never implied.
Decision tables test facts about an entity and its children. A rule over a collection must say whether every child has to match or just one, and every answer carries the trace of the rules it checked.
- id: credit-hold-allowed-stock
when:
customer.active: true
customer.credit_hold: true
item.stock_type: { all_in: $allowed_on_credit_hold }
then: ALLOW- factscredit hold · items STOCKED, DROP_SHIP
- bindingdivision south · allowed on hold: STOCKED, SPECIAL_ORDER
- rule 1good-credit · failed on
customer.credit_hold✕ - rule 2credit-hold-allowed-stock · failed on
item.stock_type(all_in)✕ - defaultNo rule matched, so the policy default applies: DENY. One drop-ship item is enough to fail “all”.
- Typos fail
- A misspelled key, node or fact is an error at build time and at run time, with a suggestion, never a silent no-op:
quota.max_itmesfails with the suggestionquota.max_items. - No guessing
- What happens on an unknown node, a missing fact or a provider outage is declared in the schema, not defaulted. Presets can tighten checks but never turn off an engine guarantee.
- Provenance
- Every result names each contributing scope, the winner, the matching rule and the bundle version. There is no API that reads one level’s raw value.
- Canonical
- The same sources compile to the same bytes, and a bundle’s name is the SHA-256 of its canonical content. Every loader checks it.
- Conformance
- 1,106 normative clauses, 576 evaluation vectors and 582 compile cases, passed by both evaluators. The nightly differential run has found no differences.
- Fast
- In-process
resolveanddecidetake about 4 to 7 µs at p99 on a bundle of 1,000 keys and 50 features. The target is under 100 µs. - Keeps running
- The Spring Boot starter and Node loader refresh by ETag, swap atomically and keep the last good bundle when a refresh fails.
| If you need… | Use |
|---|---|
| Release toggles, experiments, kill switches | A feature-flag tool. Tierwise can sit behind OpenFeature as a provider. |
| Authorization: who may do what to which resource | Cedar or OPA |
| A general-purpose configuration language | CUE, Pkl or Jsonnet |
| Typed settings across levels you define, a merge rule per key, entity-aware decisions, provenance, Java and JavaScript | Tierwise |
Status
Built, tested, not yet shipped.
Specification 0.1 is frozen and bundle format 1 is implemented in Java and JavaScript. The 0.1.0 release is waiting on publishing steps, not code.
Done
- Hosted CI green on the Java reactor and the JavaScript packages
- Nightly differential run: 10,000 cases, no difference
- Release dry run: native binaries for Linux, macOS and Windows, Docker images, SBOMs
- Both quickstarts pass on a fresh machine
Before 0.1.0
- Maven Central namespace and npm scope
- Signing keys for artifacts
- The first scheduled nightly run
- Publishing the binaries, images and docs site
Get started
From a clone to a checked bundle.
Building needs a full JDK 21; the CLI and core then run on Java 17 or newer. The JavaScript evaluator runs on Node 20+ and in browsers.
Full quickstart-
1
Build the CLI.
Until 0.1.0 is published, the CLI is a fat JAR you build from the repository.
git clone https://github.com/djkeshawa/visp-tierwise && cd visp-tierwise (cd java && ./mvnw -B -ntp -DskipTests package) alias tierwise="java -jar $(ls "$PWD"/java/tierwise-cli/target/tierwise-cli-*-all.jar)" -
2
Start a project and check it.
initwrites a minimal, valid project from a built-in preset. Try misspelling a key and runvalidateagain.tierwise init --preset=entitlements my-config tierwise validate my-config tierwise test my-config -
3
Compile, then evaluate.
Load the bundle with
tierwise-corein Java or@tierwise/evaluatorin Node, and ask it questions.tierwise compile -o bundle.json my-config
Keep going
Back to the start · 01
Visp Memory
Decisions, lessons, and goals from your project, recalled for your agent only when they matter.
Explore