menuconform
Could an AI agent build a valid, correctly priced, makeable cart from your restaurant menu?
menuconform is an open-source conformance kit for restaurant menus entering the
Universal Commerce Protocol food-ordering world. Feed it a menu
(Menu IR JSON, or an NCR Voyix Menu API export via --from ncr-menu) and it produces
a scored 0–100 report with per-rule findings and fix hints written for menu owners,
not just engineers.
$ menuconform check menu.json
menuconform 0.1.0 · rules 0.1.0 · reference time 2026-08-30T00:00:00Z
Menu: Bucky's Pizza & Wings — All Day (IR 0.2, USD, America/New_York)
Score: 0/100 errors: 17 warns: 14 infos: 16
CART-001 [error] item/itm_wings_6
no selection satisfies this item's modifier constraints — it cannot be ordered
fix: See the accompanying CONFLICT/STRUCT findings on this item's groups; ...
PRICE-002 [warn] modifier_group/mg_pizza_toppings12
option mod_extra_cheese12 conditional-price matrix does not cover mod_size_xl12 ...
fix: Add the missing matrix entries — this is the classic 'new size added,
topping table not updated' undercharge.
Why this exists
Agent ordering is arriving faster than menu data is getting ready for it. The official
UCP conformance suite
is the final protocol check: pytest against a running UCP merchant server, with a
flat id,title,price product model and (today) no food-domain coverage. menuconform
is the pre-flight check for the menu data itself: no server required, food
semantics only — modifier constraint satisfiability, nested-group depth, half/half
slot pricing, size-conditional prices, availability windows, allergen and
age-restriction completeness.
The two are complementary: menuconform → build your UCP endpoint → official conformance suite.
What it checks
Menus are validated against a language-agnostic Menu IR (JSON Schema 2020-12, in
schema/menu_ir.schema.json), designed from a field
study of the Uber Eats, DoorDash, Grubhub, and Checkmate menu models. The schema
checks shape only; ~30 stable rules
(rules/catalog.json) check meaning:
| Family | Checks |
|---|---|
STRUCT- |
schema validity, dangling references, duplicate IDs, orphans, nesting depth (>6 error, >4 warn) |
CONFLICT- |
unsatisfiable constraints: min>max, required-empty groups, defaults vs limits, invalid conditional-price triggers |
PRICE- |
negative prices, incomplete topping-by-size matrices, reachable negative totals |
AVAIL- |
windows that contradict parent scopes, never-available declarations, retirement-via-86 |
NAME- |
empty names, case-insensitive duplicates, back-of-house junk (86 DNU, zzz DO NOT USE) |
ALLERGEN- |
missing or contradictory allergen declarations (US big-9 ∪ EU-14 vocabulary) |
AGE- |
alcohol-indicative items without min_age, age-restricted items in kids categories |
CART- |
the cart solver: items no valid cart can be built for, defaults that violate their own constraints, no-positive-total items, equivalent carts that price differently |
EXPORT- |
lossiness against the pinned UCP draft (roadmap) |
The cart solver is the differentiator: it does exact constraint satisfiability with recursive option selectability, so it catches menus that are statically clean but unorderable — the class of problem no field-by-field validator sees.
Scoring
score = max(0, 100 − Σ deductions): error −5, warn −2, info −0.5, at most 15 points
per rule, then hard caps — any cart-blocking finding caps the score at 49, a
schema-invalid document at 59. Deterministic: the only time input is an injectable
reference_time, stated in the report. Same menu + same rules version = same score.
Install & use
Not yet on RubyGems (soon). From source (Ruby ≥ 3.1):
git clone https://github.com/amitkssolanki/menuconform && cd menuconform
bundle install
bundle exec exe/menuconform check fixtures/12_legacy_sprawl/menu.json
CLI:
menuconform check MENU.json [--from FORMAT] [--json] [--reference-time ISO8601] [--fail-below SCORE]
menuconform import --from FORMAT SOURCE.json [--config PATH] [-o OUT.json]
menuconform rules
menuconform version
Importers
--from ncr-menu ingests an NCR Voyix Menu API v2 menu-details payload
(nep-service-version 2A:1): link groups map onto the IR's two constraint axes,
Aloha-style price levels (group-scoped prices[].linkGroupId) lower to per-link
price overrides, freeQuantity becomes included_quantity, and per-item
itemCustomModifiers defaults are applied where unambiguous. Everything lossy
(prep-code price multipliers, step quantities, item factors, scheduled prices,
promotions/quick combos) is stashed under x_ extension keys and reported as
import notes — the same list that seeds the EXPORT- lossiness story. Vendor
field mappings live in config/importers/ncr_menu.json
(set timezone to the site's IANA zone; NCR payloads don't carry one). A
sanitized sample export and its golden IR live in
fixtures/importers/ncr_menu/ — a clean NCR
import scores 98/100, because NCR's menu contract has no allergen fields at
all: that gap is exactly what agents will trip over.
Exit codes: 0 passed the gate, 1 failed (error findings, or score below
--fail-below), 2 usage/input error. Ruby API:
require "menuconform"
report = Menuconform::Engine.new.analyze(JSON.parse(File.read("menu.json")))
report.score # => 0..100
report.findings # => [Menuconform::Finding]
report.to_h # => full report hash
UCP export
menuconform export MENU.json maps the IR onto the pinned UCP draft's catalog
shapes (product/variant/option/price) and reports what the export loses as
EXPORT- findings: required single-select sizes become real options/variants;
everything else — multi-select modifiers, nested groups, conditional prices,
half/half, allergens, age restrictions, schedules — is dropped, degraded, or makes
the item unrepresentable (EXPORT-001, exit 1). The full lossy-construct analysis
lives in docs/export-lossiness.md. Export findings
deliberately do not affect the check score: check judges your menu data,
export judges the current draft's fit for food.
Fixtures
fixtures/ contains twelve menus with pinned expected findings — from
01_golden_path (must score 100) to 12_legacy_sprawl, a plausible 15-year-old
franchise export that scores 0. They double as the conformance suite for the rules
themselves and as worked examples of the IR. There is currently no public sample
food menu anywhere in the UCP ecosystem; these aim to fill that gap.
UCP spec pin
The IR is validated against Menu IR v0.2. The UCP exporter (in progress) tracks UCP spec v2026-08-25 (pin checked 2026-08-30; re-pinned monthly). The IR is deliberately not coupled to the draft — menus are validated against the IR, and only the exporter chases the moving spec.
Roadmap
- More POS-family importers and a generic CSV importer
- UCP exporter +
EXPORT-lossiness rules against the pinned draft - TypeScript port if implementers need it
License & maintainer
Apache-2.0. Built and maintained by Amit Solanki. Issues and real-world menu pathologies (sanitized!) are very welcome.