menuconform

CI

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.