Skip to content

feat: Add orderly kit - #316

Merged
akshatvirmani merged 5 commits into
Lamatic:mainfrom
laxmikhengare:feat/orderly
Aug 3, 2026
Merged

feat: Add orderly kit#316
akshatvirmani merged 5 commits into
Lamatic:mainfrom
laxmikhengare:feat/orderly

Conversation

@laxmikhengare

@laxmikhengare laxmikhengare commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The problem

Four people, one nut allergy, one vegetarian, €60 between you, and a menu none of you can read. Existing tools translate the menu and hand it back. You still do the reasoning, at the table, with a waiter standing there.

Google Lens already does live camera translation in 100+ languages, free, on a phone you own. It does not know who is at your table, what they cannot eat, or what your budget is. That is the gap Orderly fills; translation is table stakes underneath it, not the point of it.

The approach

The model reads. The solver decides.

One vision flow reads the photo and reports what it saw. Every decision after that is deterministic, unit-tested code: EU-14 allergen mapping, dietary rules, price parsing, and a solver that turns a party plus a budget into one order.

No language model is asked what is safe to eat.

What the solver guarantees

  • Allergen conflicts are eliminated from the candidate pool before optimisation begins. They are structurally unreachable, not merely penalised.
  • The budget is a hard ceiling; infeasible is reported rather than faked.
  • Unpriced dishes are excluded and reported, never treated as free.
  • Every dish appears exactly once in the order or the rejections, with a reason.

It does not guarantee optimality. The selection is greedy set cover with one improvement pass. The README says so rather than overclaiming.

Tradeoffs

  • The engine lives in the app rather than a codeNode, so it can be unit-tested. For a safety tool, testability beats flow portability.
  • Shared plates escalate allergies to the whole table (cross-contamination); diets stay per-diner.
  • No currency conversion. Mismatched prices are excluded rather than converted at a stale rate.
  • The rate limiter is per-process. Fine for a demo, and stated as a limitation.

Verification

192 tests across 6 suites. Typecheck and production build clean. Verified end-to-end against the live deployed flow: 15 dishes read, both fish dishes excluded by name for the diner avoiding fish, order returned under budget with everyone fed.

  • Added the Orderly kit configuration, metadata, environment templates, ignore rules, README, agent guide, and constitution.
  • Added the menu-scan flow with trigger, multimodal LLM, Instructor LLM, structured response, and API response mapping nodes.
  • The flow accepts a menu image, source language, and target language.
  • The multimodal LLM extracts visible dishes, names, descriptions, ingredients, prices, categories, confidence values, detected language, currency, and menu notes.
  • The Instructor LLM converts the extracted content into structured JSON.
  • Added Claude Haiku 4.5 model configurations for both LLM nodes.
  • Added deterministic EU-14 allergen mapping with multilingual, bounded, longest-match, and provenance-aware detection.
  • Added dietary classification for vegetarian, vegan, halal, and gluten-free requirements.
  • Added localized price parsing, currency validation, formatting, and total calculation.
  • Added deterministic table planning with allergen filtering, dietary rules, hard budget limits, unpriced-dish rejection, shared-plate escalation, diner coverage, auditability, and one improvement pass.
  • Added server orchestration for scanning, image uploads, Lamatic flow execution, safe errors, upload availability, and in-memory rate limiting.
  • Added SSRF-safe image URL validation, duplicate diner-ID validation, mixed-currency validation, per-diner dislikes, bounded keyword caching, and robust upload and scan state handling.
  • Added a Next.js application with menu input, language selection, diner preferences, budget controls, serving modes, scan progress, errors, table plans, dish details, warnings, and safety disclaimers.
  • Added reusable UI components and global styling for controls, diner settings, dish cards, table plans, verdicts, and non-Latin text.
  • Added shared domain types, schemas, keyword matching, Lamatic client utilities, blob upload validation, and Tailwind utilities.
  • Added six test suites covering allergen logic, allergen tables, dietary rules, prices, scan schemas, and table solving.

Photograph a menu in any language and get one concrete order for your
table — not a translated menu, an order.

## The design

The model reads. The solver decides.

A single vision flow reads the menu photo and reports what it saw:
names, transliterations, translations, inferred ingredients, printed
prices, and a legibility confidence per line. Every decision after that
is made by deterministic, unit-tested code — what contains an allergen,
what each diner may eat, and what the table should order.

No language model is asked what is safe to eat.

## What the solver guarantees

- Allergen conflicts are eliminated from the candidate pool before
  optimisation begins, so no budget or variety pressure can reintroduce
  them. The guarantee is structural, and asserted on output.
- The budget is a ceiling. When no order fits, it says so and returns
  nothing rather than offering a plausible over-budget suggestion.
- Unpriced dishes are excluded and reported, never treated as free.
- Every dish appears exactly once in either the order or the rejection
  list, each with a human-readable reason.

It does not guarantee optimality — the selection is a deterministic
greedy heuristic with one improvement pass. The README says so.

## Notable behaviour

When plates are shared, an allergen anywhere on the table is a risk to
everyone, so a conflicting dish is excluded outright rather than merely
withheld from one diner. Dietary requirements do not escalate this way:
a vegetarian is not harmed by someone else ordering pork.

An allergen is reported as "contains" only when named in the dish title,
which the restaurant printed. Anything inferred from the model's guessed
ingredient list is "may contain". Both disqualify equally — the
distinction informs the diner, it never relaxes the constraint.

## Verification

165 tests across 6 suites. Typecheck and production build clean.
Verified end-to-end against a live deployed flow: 15 dishes read, both
fish dishes excluded by name for the diner avoiding fish, order returned
under budget with everyone fed.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e6cd10f1-aa8f-46b9-a7d8-043147e645be

📥 Commits

Reviewing files that changed from the base of the PR and between c69db3d and aac459e.

📒 Files selected for processing (3)
  • kits/orderly/README.md
  • kits/orderly/apps/__tests__/price.test.ts
  • kits/orderly/apps/lib/price.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

Changes

Orderly adds a menu-scanning kit with multimodal extraction, deterministic safety and price logic, constrained table planning, uploads, server orchestration, a Next.js interface, tests, configuration, and documentation.

Orderly menu intelligence

Layer / File(s) Summary
Domain contracts and deterministic rules
kits/orderly/apps/lib/types.ts, kits/orderly/apps/lib/allergen-table.ts, kits/orderly/apps/lib/diet-rules.ts, kits/orderly/apps/lib/price.ts, kits/orderly/apps/lib/keyword-match.ts, kits/orderly/apps/__tests__/*
Defines domain types and deterministic allergen, dietary, keyword, and price behavior with focused tests.
Dish enrichment and table solver
kits/orderly/apps/lib/allergen-engine.ts, kits/orderly/apps/lib/table-solver.ts, kits/orderly/apps/__tests__/allergen-engine.test.ts, kits/orderly/apps/__tests__/table-solver.test.ts
Enriches dishes with safety and price data, then plans shared or individual orders with budgets, diner coverage, rejection details, and deterministic output.
Scan contracts and server orchestration
kits/orderly/apps/lib/scan-schema.ts, kits/orderly/apps/lib/lamatic-client.ts, kits/orderly/apps/lib/rate-limit.ts, kits/orderly/apps/lib/blob-upload.ts, kits/orderly/apps/actions/orchestrate.ts
Validates requests and model results, executes the configured flow, applies rate limits, supports optional image uploads, and returns user-safe scan responses.
Menu-scan flow and safety configuration
kits/orderly/flows/menu-scan.ts, kits/orderly/prompts/*, kits/orderly/model-configs/*, kits/orderly/constitutions/default.md, kits/orderly/lamatic.config.ts
Defines the multimodal extraction flow, structured response mapping, model settings, prompts, safety constitution, and kit metadata.
Next.js interface
kits/orderly/apps/app/*, kits/orderly/apps/components/*, kits/orderly/apps/package.json, kits/orderly/apps/*config*, kits/orderly/apps/tsconfig.json
Adds the scan page, party controls, order-plan and dish rendering, safety disclaimer, styling, reusable UI components, and application configuration.
Setup and operational documentation
kits/orderly/README.md, kits/orderly/agent.md, kits/orderly/.env.example, kits/orderly/apps/.env.example, kits/orderly/.gitignore, kits/orderly/apps/.gitignore
Documents setup, environment variables, workflow behavior, safety limits, troubleshooting, and local files excluded from version control.

Suggested reviewers: amanintech, d-pamneja

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: adding the Orderly kit.
Description check ✅ Passed The description clearly explains the problem, approach, guarantees, tradeoffs, limitations, and verification for the Orderly kit.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

:robot_face: AgentKit Structural Validation

No contribution files detected in this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kits/orderly/apps/.env.example (1)

1-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two identical env templates, one source of truth needed. kits/orderly/apps/.env.example and kits/orderly/.env.example carry the exact same variable list and comments. Nothing enforces that a future edit to one propagates to the other, and agent.md's quickstart only tells contributors to copy the apps/ file, so the root copy has no clear owner.

  • kits/orderly/apps/.env.example#L1-L31: keep this file as the canonical, guideline-required template for the Next.js app.
  • kits/orderly/.env.example#L1-L31: remove this duplicate, or replace its body with a short pointer to apps/.env.example so the two cannot drift apart.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/orderly/apps/.env.example` around lines 1 - 31, Keep
kits/orderly/apps/.env.example as the canonical environment template and leave
its contents unchanged. Update kits/orderly/.env.example to remove the duplicate
variable definitions and comments, replacing them with a short pointer to
apps/.env.example so contributors use the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kits/orderly/apps/__tests__/allergen-table.test.ts`:
- Around line 158-163: Replace the tautological duplicate-call comparison in the
determinism test with an assertion against the exact allergen output for the
existing four-allergen fixture, confirming the documented KEYS_LONGEST_FIRST
ordering. Verify the expected array from a local test run, and leave the broader
keyword-match coverage request out of scope.

In `@kits/orderly/apps/app/page.tsx`:
- Around line 33-43: Replace the manual form state and validation in HomePage
with react-hook-form using a zod schema covering imageUrl, targetLanguage, and
diners, including URL validation and the required diners.length constraint.
Connect the photo upload, image URL, language select, and scan trigger through
the form APIs, and derive scan eligibility and submission values from validated
form state instead of the current canScan logic and individual useState fields.
- Around line 45-62: Update handleFile to catch rejected uploadMenuPhoto calls,
set a user-visible error, and always reset uploading to false even when the
server action throws. Preserve the existing result.ok handling for successful
responses and returned failures.
- Around line 123-137: Validate imageUrl before it reaches
executeStep("menu-scan") and menuImage: require HTTPS and reject loopback,
private, link-local, unspecified, and other special IP or host values while
allowing public HTTPS image URLs. Add the custom Zod validation alongside
ScanRequestSchema, reuse it for request parsing, and cover accepted and rejected
URL cases with explicit tests.
- Around line 64-82: Update handleScan so the UI remains disabled for the entire
scanMenu request, not just the synchronous portion of startTransition. Add and
maintain a manual scanning state around the async call, include it in the
existing canScan/busy gating, and clear it after both successful and failed
responses so the “Plan our order” button re-enables only when the request
completes.

In `@kits/orderly/apps/components/party-panel.tsx`:
- Around line 59-62: Add per-diner dislike input handling in the PartyPanel
component alongside the existing allergy and diet controls, wiring it to the
diner’s dislikes state so user entries update and persist the dislikes array
instead of leaving it empty. Reuse the existing state/update patterns and
allergen-related UI symbols in PartyPanel.
- Around line 52-64: Update addDiner to generate collision-resistant diner IDs,
replacing the Date.now-based value with crypto.randomUUID() or a reliable
incrementing counter. Preserve the existing ID usage by updateDiner,
removeDiner, and the React list key.

In `@kits/orderly/apps/lib/allergen-engine.ts`:
- Around line 60-65: Update the Pass 1 input used by detectAllergens so nameText
contributes only raw.nameOriginal, excluding model-generated nameTransliterated
and nameTranslated from firm “contains” matching. Preserve the existing
filtering behavior for a non-empty original name and leave other detection
passes unchanged.

In `@kits/orderly/apps/lib/allergen-table.ts`:
- Around line 254-261: Update matchAllergens to normalize the input text once,
then pass the normalized value to an exported matcher that accepts
pre-normalized text. In the KEYS_LONGEST_FIRST loop, test the compiled patterns
directly through that matcher instead of invoking containsKeyword in a way that
re-normalizes the string. Preserve the existing seen allergen handling and
longest-key matching behavior.
- Around line 249-267: Update matchAllergens or its containsKeyword matching
path to suppress an allergen when the matched key is immediately followed by an
explicit free-from qualifier such as “free,” while preserving normal matches and
existing word-boundary behavior. Escape keys before using them in any
interpolated pattern, keep the qualifier list short and auditable, and add
regression cases alongside the existing allergen word-boundary tests.

In `@kits/orderly/apps/lib/diet-rules.ts`:
- Around line 203-215: Add rennet detection to the halal classification flow
using the existing HALAL_UNCERTAIN and nothingDisqualifying symbols: include
HALAL_UNCERTAIN in the prerequisite check, then treat a matching rennet
ingredient like meat or seafood by assigning halal to "unknown" and adding an
appropriate preparation-status reason. Preserve the existing false outcomes for
pork and alcohol and the all-unknown short-circuit behavior.
- Around line 233-247: Update verdictForDiet to import and use the existing
DietId type from types.ts instead of restating the diet union, and ensure the
switch is exhaustiveness-checked so newly added DietId members such as kosher
require an explicit verdict mapping.

In `@kits/orderly/apps/lib/keyword-match.ts`:
- Around line 94-96: Update findKeyword in keyword-match.ts to avoid sorting the
same keyword array on every call. Cache the length-descending,
lexicographically-tiebroken ordering by array identity using a WeakMap, or
expose a prepareKeywords helper and have the keyword tables initialize their
ordered lists once; preserve findKeyword’s matching behavior.
- Around line 43-63: Bound PATTERN_CACHE so dynamically supplied keywords cannot
create unbounded process-lifetime memory growth. Update patternFor to enforce a
maximum cache size and evict an existing entry before adding a new pattern once
the limit is reached, while preserving cached reuse and current matching
behavior.
- Around line 56-63: Update patternFor to normalize the keyword with
normalizeText before compiling the RegExp, and use that normalized value for
both PATTERN_CACHE lookup/storage and pattern construction. Keep the existing
matching behavior unchanged for already-normalized module-level keywords.

In `@kits/orderly/apps/lib/price.ts`:
- Around line 197-202: Move the zeroDecimal Set from formatPrice to module scope
beside ISO_CODES, then update formatPrice to use Intl.NumberFormat for non-empty
currencies so currency-specific fraction digits, symbols, and digit grouping are
handled automatically. Preserve the plain numeric formatting path when
price.currency is "".
- Around line 65-82: Update the ISO-code matching in detectCurrency so currency
codes immediately followed or preceded by digits are recognized, while still
requiring string edges or non-digit delimiters. Replace the current
word-boundary-based pattern around each code in the ISO_CODES loop; keep the
symbol pass and existing return behavior unchanged.
- Around line 173-189: Update the amount selection in the price parsing function
after `values` is built: use `Math.max(...values)` only when the parsed input
contains a range separator joining the tokens; otherwise use the final parsed
value (`values[values.length - 1]`). Preserve the existing validation and
currency detection behavior.

In `@kits/orderly/apps/lib/scan-schema.ts`:
- Around line 34-40: Update the diners validation in ScanRequestSchema with a
refine check that requires every diner.id to be unique, rejecting duplicate IDs
before solver processing while preserving the existing DinerSchema validation
and error behavior.
- Around line 49-61: Update imageUrl validation in ScanRequestSchema to accept
only https URLs, and reject non-fetchable paste-only schemes such as blob: and
data:. If uploads must originate from storage, additionally enforce the approved
Vercel Blob hostname before scanMenu receives request.imageUrl.

In `@kits/orderly/apps/lib/table-solver.ts`:
- Around line 517-525: Rename the local variables aNew and bNew in the
affordable.sort comparator to reflect that categories.has(...) identifies
already represented (old) categories, while preserving the existing ascending
comparison and all subsequent tie-breakers.
- Around line 247-259: Update the Stage 0 dish-filtering flow to resolve a
non-empty reference currency from the budget when available, otherwise from the
first readable priced dish, and reject currency mismatches even when budget is
null. Use that same reference for candidate filtering and preserve
currency-mismatch rejection details; add a regression test covering a
mixed-currency menu without a budget and asserting the rejection.

In `@kits/orderly/README.md`:
- Line 47: Update both fenced text blocks in the README, including the ASCII
diagram and sample output, to add a text language tag to their opening fences so
they satisfy markdownlint MD040; leave the block contents unchanged.
- Around line 56-62: Update both documented test counts in
kits/orderly/README.md: change the architecture summary at lines 56-62 from 158
to 165 tests, and update the Scripts section at lines 194-199 to state 165 tests
across 6 suites.

---

Outside diff comments:
In `@kits/orderly/apps/.env.example`:
- Around line 1-31: Keep kits/orderly/apps/.env.example as the canonical
environment template and leave its contents unchanged. Update
kits/orderly/.env.example to remove the duplicate variable definitions and
comments, replacing them with a short pointer to apps/.env.example so
contributors use the single source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dcd01a3d-53dd-44da-a252-559b06054269

📥 Commits

Reviewing files that changed from the base of the PR and between c12bd02 and 3e1f2f2.

⛔ Files ignored due to path filters (2)
  • kits/orderly/apps/package-lock.json is excluded by !**/package-lock.json
  • kits/orderly/assets/demo/menu-1.jpg is excluded by !**/*.jpg
📒 Files selected for processing (49)
  • kits/orderly/.env.example
  • kits/orderly/.gitignore
  • kits/orderly/README.md
  • kits/orderly/agent.md
  • kits/orderly/apps/.env.example
  • kits/orderly/apps/.gitignore
  • kits/orderly/apps/__tests__/allergen-engine.test.ts
  • kits/orderly/apps/__tests__/allergen-table.test.ts
  • kits/orderly/apps/__tests__/diet-rules.test.ts
  • kits/orderly/apps/__tests__/price.test.ts
  • kits/orderly/apps/__tests__/scan-schema.test.ts
  • kits/orderly/apps/__tests__/table-solver.test.ts
  • kits/orderly/apps/actions/orchestrate.ts
  • kits/orderly/apps/app/globals.css
  • kits/orderly/apps/app/layout.tsx
  • kits/orderly/apps/app/page.tsx
  • kits/orderly/apps/components.json
  • kits/orderly/apps/components/disclaimer-banner.tsx
  • kits/orderly/apps/components/dish-card.tsx
  • kits/orderly/apps/components/party-panel.tsx
  • kits/orderly/apps/components/table-plan-view.tsx
  • kits/orderly/apps/components/ui/button.tsx
  • kits/orderly/apps/components/ui/input.tsx
  • kits/orderly/apps/components/ui/label.tsx
  • kits/orderly/apps/lib/allergen-engine.ts
  • kits/orderly/apps/lib/allergen-table.ts
  • kits/orderly/apps/lib/blob-upload.ts
  • kits/orderly/apps/lib/diet-rules.ts
  • kits/orderly/apps/lib/keyword-match.ts
  • kits/orderly/apps/lib/lamatic-client.ts
  • kits/orderly/apps/lib/price.ts
  • kits/orderly/apps/lib/rate-limit.ts
  • kits/orderly/apps/lib/scan-schema.ts
  • kits/orderly/apps/lib/table-solver.ts
  • kits/orderly/apps/lib/types.ts
  • kits/orderly/apps/lib/utils.ts
  • kits/orderly/apps/next.config.mjs
  • kits/orderly/apps/package.json
  • kits/orderly/apps/postcss.config.mjs
  • kits/orderly/apps/tsconfig.json
  • kits/orderly/constitutions/default.md
  • kits/orderly/flows/menu-scan.ts
  • kits/orderly/lamatic.config.ts
  • kits/orderly/model-configs/menu-scan_instructor-llmnode-342_generative-model-name.ts
  • kits/orderly/model-configs/menu-scan_multi-modal-llmnode-968_generative-model-name.ts
  • kits/orderly/prompts/menu-scan_instructor-llmnode-342_system_0.md
  • kits/orderly/prompts/menu-scan_instructor-llmnode-342_user_1.md
  • kits/orderly/prompts/menu-scan_multi-modal-llmnode-968_system_0.md
  • kits/orderly/prompts/menu-scan_multi-modal-llmnode-968_user_1.md

Comment thread kits/orderly/apps/__tests__/allergen-table.test.ts Outdated
Comment thread kits/orderly/apps/app/page.tsx
Comment thread kits/orderly/apps/app/page.tsx
Comment thread kits/orderly/apps/app/page.tsx
Comment thread kits/orderly/apps/app/page.tsx
Comment thread kits/orderly/apps/lib/scan-schema.ts
Comment thread kits/orderly/apps/lib/table-solver.ts
Comment thread kits/orderly/apps/lib/table-solver.ts
Comment thread kits/orderly/README.md Outdated
Comment thread kits/orderly/README.md Outdated
Security
- Validate imageUrl before it reaches the vision node. The flow fetches the
  URL server-side, so an unvalidated value made this a request proxy. Now
  https-only, with loopback, private, link-local, carrier-grade NAT, IPv6
  unique-local and cloud metadata hosts rejected. 6 new test cases.

Correctness
- Reject duplicate diner IDs. The solver keys coverage off a Set of IDs, so
  duplicates silently counted two people as one and left someone unfed
  without ever appearing in `unfed`.
- Reject mixed-currency menus when no budget is set. The check previously
  only ran with a budget, but the plan still reports a total, and a total
  summed across currencies is meaningless either way.
- Suppress allergen matches negated by an immediately following "free", so
  "egg-free" no longer reports eggs. A dish was being flagged precisely
  because it advertised being safe.
- Normalise keywords before compiling their patterns, so a caller-supplied
  dislike like "Soy-Sauce" can match normalised text.
- handleFile now resets uploading state in a finally block. A throwing
  server action previously left the form disabled permanently.
- handleScan tracks its own scanning state. startTransition only spans the
  synchronous part of an async callback, so the button re-enabled mid-request.
- Diner IDs use crypto.randomUUID instead of Date.now, which collided when
  two diners were added in the same millisecond.

Completeness
- Wire up the per-diner dislikes input. The field existed in the type and was
  read by the engine but had no UI, so it was always empty.

Performance
- matchAllergens normalises its input once rather than once per table key,
  ~180 times per call.
- findKeyword caches keyword ordering by array identity instead of re-sorting
  the same constant tables on every call.
- Bound the pattern cache, since diner dislikes are unbounded free text.

Clarity
- verdictForDiet uses the DietId type with an exhaustiveness guard.
- Rename aNew/bNew to aRepresented/bRepresented; the predicate identifies
  categories already on the table, so the old names read backwards.
- Document why a translated dish name still counts as "contains".
- README: label bare code fences, correct stale test counts.

178 tests, 13 new. Typecheck and build clean. Re-verified end-to-end against
the live flow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kits/orderly/README.md (1)

79-87: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Mission check: reconcile the exclusion count.

EXCLUDED FROM THIS ORDER (3) lists three entries, but the final entry has no dish name. The following warning reports two more unpriced dishes without placing them in the rejection list.

If the unpriced dishes are additional exclusions, include them in the list and update the count. If they are part of the three exclusions, name them and restructure the example.

Based on the PR objective, every dish must appear exactly once in the order or rejection list with a reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/orderly/README.md` around lines 79 - 87, Update the README example’s
excluded-order section so every dish appears exactly once in either the order or
rejection list with a reason. Reconcile the “EXCLUDED FROM THIS ORDER” count and
entries with the two unpriced dishes reported by the warning, naming those
dishes when they are included or restructuring the example if they are already
represented.
♻️ Duplicate comments (1)
kits/orderly/apps/lib/keyword-match.ts (1)

82-99: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Agent, a blank keyword is a live wire wired to every dish on the table.

Trace patternFor(""). normalized becomes "", and the compiled pattern is \b(?:e?s)?\b(?!\s+(?:free)\b). The optional group can match zero-width, so both \b assertions collapse onto the same position. That position exists at the start of almost any non-empty haystack that is not immediately followed by "free ". normalizedTextHasKeyword only guards haystack === ""; it does not guard an empty keyword.

This is not hypothetical. findKeywordInAny is called with diner.dislikes as the keyword list. A blank or punctuation-only dislike entry (for example " " or "-") normalizes to "" here, and every dish on the menu would then match that "keyword." The earlier review round already proposed exactly this guard (if (canonical === "") return /(?!)/;) alongside the normalization fix, but it was left out when the normalization fix landed.

Add the guard back.

🛡️ Proposed fix
 function patternFor(keyword: string): RegExp {
   const normalized = normalizeText(keyword);
+  if (normalized === "") return /(?!)/;

   let pattern = PATTERN_CACHE.get(normalized);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/orderly/apps/lib/keyword-match.ts` around lines 82 - 99, Update
patternFor so that after normalizeText(keyword) produces an empty canonical
value, it immediately returns a never-matching regular expression such as the
established /(?!)/ guard, before consulting or populating PATTERN_CACHE;
preserve the existing pattern construction and caching behavior for non-empty
normalized keywords.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kits/orderly/apps/components/party-panel.tsx`:
- Around line 57-60: Export a diner-ID helper near the existing ID generation in
addDiner that uses crypto.randomUUID when available and otherwise returns a
Date.now()-based value with a random suffix. Update addDiner to call this helper
so insecure browser contexts no longer throw while preserving unique IDs for
diners added within the same millisecond.
- Around line 182-197: Update the dislikes field in the diner card to use a
dedicated DislikesField component with local raw-text state, preserving commas
and empty segments while the operator edits. Normalize the text into trimmed,
non-empty entries capped at 20 only when notifying the parent through onChange,
and render the component with diner, disabled, and updateDiner-backed props.

In `@kits/orderly/apps/lib/scan-schema.ts`:
- Around line 67-102: Update BLOCKED_IP_PATTERNS and isPubliclyFetchableImageUrl
to recognize IPv4-mapped IPv6 addresses, including hexadecimal and equivalent
mapped forms, and reject them when the embedded IPv4 address is loopback,
private, link-local, unspecified, or otherwise blocked. Add a test covering
https://[::ffff:127.0.0.1]/menu.jpg and preserve acceptance of genuinely public
image URLs.

---

Outside diff comments:
In `@kits/orderly/README.md`:
- Around line 79-87: Update the README example’s excluded-order section so every
dish appears exactly once in either the order or rejection list with a reason.
Reconcile the “EXCLUDED FROM THIS ORDER” count and entries with the two unpriced
dishes reported by the warning, naming those dishes when they are included or
restructuring the example if they are already represented.

---

Duplicate comments:
In `@kits/orderly/apps/lib/keyword-match.ts`:
- Around line 82-99: Update patternFor so that after normalizeText(keyword)
produces an empty canonical value, it immediately returns a never-matching
regular expression such as the established /(?!)/ guard, before consulting or
populating PATTERN_CACHE; preserve the existing pattern construction and caching
behavior for non-empty normalized keywords.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70c7d152-5b8e-4666-a94e-38487521f5b2

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1f2f2 and c8310af.

📒 Files selected for processing (12)
  • kits/orderly/README.md
  • kits/orderly/apps/__tests__/allergen-table.test.ts
  • kits/orderly/apps/__tests__/scan-schema.test.ts
  • kits/orderly/apps/__tests__/table-solver.test.ts
  • kits/orderly/apps/app/page.tsx
  • kits/orderly/apps/components/party-panel.tsx
  • kits/orderly/apps/lib/allergen-engine.ts
  • kits/orderly/apps/lib/allergen-table.ts
  • kits/orderly/apps/lib/diet-rules.ts
  • kits/orderly/apps/lib/keyword-match.ts
  • kits/orderly/apps/lib/scan-schema.ts
  • kits/orderly/apps/lib/table-solver.ts

Comment thread kits/orderly/apps/components/party-panel.tsx Outdated
Comment thread kits/orderly/apps/components/party-panel.tsx Outdated
Comment thread kits/orderly/apps/lib/scan-schema.ts
@laxmikhengare

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. I have gone through every finding and verified each against the current code. 15 fixed, 5 declined with reasons below.

Pushed in c8310af. 178 tests (13 new), typecheck clean, production build clean, and re-verified end to end against the live deployed flow.

Fixed

Security

  • imageUrl validation. This was the most important finding. The flow fetches menuImage server-side, so an unvalidated URL made this a request proxy: a caller could aim it at cloud metadata, a loopback service, or something inside a private network. Now https-only, with loopback, private ranges, link-local (including 169.254.169.254), carrier-grade NAT, IPv6 unique-local, and known metadata hostnames rejected. Exposed as isPubliclyFetchableImageUrl, enforced by ScanRequestSchema, and covered by 6 test cases for both accepted and rejected forms.

Correctness

  • Duplicate diner IDs. The solver keys coverage off a Set of diner IDs, so duplicates counted two people as one and could leave someone unfed without them appearing in unfed. A silent wrong answer, now rejected by a refine on the diners array.
  • Currency mismatch without a budget. The check only ran when a budget was set, but the plan reports a total regardless, and a total summed across two currencies is meaningless either way. Now resolved against a reference currency taken from the budget when present and otherwise from the first readable priced dish. Regression test added.
  • Free-from qualifiers. "egg-free" was reporting eggs, so a dish got flagged precisely because it advertised being safe. Real for egg-free, milk-free, soy-free. Suppressed via a negative lookahead in the compiled pattern, with the qualifier list kept short and auditable. Three regression cases added, including one confirming that "free-range egg" still matches.
  • Keyword normalisation in patternFor. Keywords are now normalised the same way haystacks are, so a caller-supplied dislike like "Soy-Sauce" can actually match "soy sauce".
  • handleFile error handling. A throwing server action left uploading true permanently and the form dead. Now wrapped in try/catch/finally.
  • handleScan gating. startTransition only spans the synchronous portion of an async callback, so pending dropped and the button re-enabled mid-request. Added an explicit scanning flag covering the whole call.
  • Diner ID collisions. Date.now() collides when two diners are added inside the same millisecond, which fed directly into the duplicate-ID bug above. Now crypto.randomUUID().

Completeness

  • Dislikes input. Good catch. The field existed in the type and the engine read it, but nothing in the UI set it, so it was always empty. Now a per-diner comma-separated input, capped at 20 entries to match the schema.

Performance

  • matchAllergens normalises its input once rather than once per table key, which was roughly 180 redundant normalisations per call.
  • findKeyword caches keyword ordering in a WeakMap keyed on array identity instead of re-sorting the same module-level constants on every call.
  • PATTERN_CACHE is now bounded with oldest-first eviction, since diner dislikes are unbounded free text.

Clarity

  • verdictForDiet imports DietId and has an exhaustiveness guard, so adding a member without mapping it is a compile error.
  • Renamed aNew / bNew to aRepresented / bRepresented. The predicate identifies categories already on the table, so the old names read backwards.
  • README: added text language tags to the two unlabelled opening fences (MD040), and corrected the stale test counts.

Declined, with reasons

react-hook-form refactor of HomePage. A large rewrite of working code plus a new dependency, for no behavioural change. The two concrete problems it was aimed at, input validation and disabled-state gating, are both fixed directly: validation now lives in ScanRequestSchema including the URL rules above, and the scanning state fix covers the button.

Restricting Pass 1 to nameOriginal. The inconsistency you spotted between the code and its comment was real, so I fixed the comment. Excluding transliterations and translations would mean no non-Latin menu could ever produce a firm "contains", and those are exactly the menus where a diner is least able to verify for themselves. A translation renders a string the restaurant chose; an inferred ingredient list does not. Both certainties disqualify equally in the solver regardless, so this only affects UI wording.

Rennet detection for halal. The suggestion references a HALAL_UNCERTAIN symbol that does not exist in this codebase. The underlying point about animal rennet is fair, but treating every cheese dish as halal-uncertain would flag most Western menus and drown the signal in noise. Noted as a known limitation rather than encoded.

Intl.NumberFormat in formatPrice. This would change documented output from 12.50 EUR to €12.50 and break the existing assertions, for a formatting preference rather than a defect. I did move the zeroDecimal set to module scope as suggested.

Last parsed value instead of Math.max for ranges. Math.max is deliberate and documented: for a budget-constrained tool, never underestimate the bill. Taking the final token has no equivalent safety argument, and both approaches are heuristics that fail on inputs like "$12.50 per 100g".

Root .env.example reduced to a pointer. I would rather leave this. CONTRIBUTING.md requires .env.example at the kit root for kits, and Phase-1 validation may parse it for variable names. Removing the contents to eliminate conventional duplication risks failing that check for a cosmetic gain.

Happy to revisit any of the declined items if you disagree. Requesting /validate when checks are green.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📡 Running Studio validation — results will appear here shortly.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Studio Runtime Validation (Phase 2)

Studio validation passed. The kit loaded successfully in Lamatic Studio.

This PR is ready for final review and merge.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Hi @laxmikhengare! 👋

Before this PR can be reviewed by maintainers, please resolve all comments and requested changes from the CodeRabbit automated review.

Steps to follow:

  1. Read through all CodeRabbit comments carefully
  2. Address each issue raised (or reply explaining why you disagree)
  3. Push your fixes as new commits
  4. Once all issues are resolved, comment here so we can re-review

This helps keep the review process efficient for everyone. Thank you! 🙏

Security
- Reject IPv4-mapped IPv6 addresses carrying a blocked IPv4. `URL` normalises
  https://[::ffff:127.0.0.1] to the hexadecimal ::ffff:7f00:1, which matched
  no IPv4 pattern and began with neither fe80 nor fc00, so it walked straight
  past the blocklist. Both spellings are now decoded and judged on the address
  they actually carry. Verified as a live bypass before the fix.

Correctness
- detectCurrency recognises ISO codes butted against digits. A word boundary
  sits between a letter and a digit, so `\bJPY\b` failed on "980JPY", a form
  both menus and OCR produce. Now bounded by non-letters instead.
- formatPrice delegates to Intl.NumberFormat, with the locale pinned to en-US
  so output does not vary by host. Removes the hand-maintained zero-decimal
  currency list, and falls back rather than throwing on a malformed code.
- Rennet sets halal to "unknown" when a menu names it explicitly. Not inferred
  from the presence of cheese, which would mark most Western dishes uncertain
  and drown the signal.
- newDinerId falls back when crypto.randomUUID is unavailable. It is only
  defined in a secure context, so the previous direct call threw when the app
  was served over plain HTTP on a LAN.

Usability
- The dislikes input holds its own raw text. Driving it from
  dislikes.join(", ") meant the change handler split and filtered on every
  keystroke, so a typed comma vanished immediately and a second entry could
  not be typed at all.

Docs
- README: correct the contains/may-contain wording to match the code, and
  update test counts.

188 tests, 10 new. Typecheck and build clean. Re-verified end to end against
the live flow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kits/orderly/apps/lib/scan-schema.ts (1)

80-143: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Mission: replace the hand-rolled IPv6 gate with an audited parser.

The ::ffff:... bypass is closed, but embeddedIPv4 still makes a security-critical SSRF decision from string slicing. This path also leaves deprecated IPv4-compatible literals like https://[::127.0.0.1]/ outside the mapped check. If the fetch/runtime can reach the embedded IPv4 address, use an RFC-aware address library such as ip-address or ipaddr.js to keep SSRF semantics from drifting with more IPv6 corner cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/orderly/apps/lib/scan-schema.ts` around lines 80 - 143, Replace the
hand-rolled embeddedIPv4 parsing in embeddedIPv4 and its use from
isPubliclyFetchableImageUrl with an audited RFC-aware IP address parser such as
the project’s existing ip-address or ipaddr.js dependency. Normalize IPv4-mapped
and IPv4-compatible IPv6 literals, including forms like ::ffff:127.0.0.1 and
::127.0.0.1, and apply BLOCKED_IP_PATTERNS to the extracted embedded IPv4
address before allowing the URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@kits/orderly/apps/lib/scan-schema.ts`:
- Around line 80-143: Replace the hand-rolled embeddedIPv4 parsing in
embeddedIPv4 and its use from isPubliclyFetchableImageUrl with an audited
RFC-aware IP address parser such as the project’s existing ip-address or
ipaddr.js dependency. Normalize IPv4-mapped and IPv4-compatible IPv6 literals,
including forms like ::ffff:127.0.0.1 and ::127.0.0.1, and apply
BLOCKED_IP_PATTERNS to the extracted embedded IPv4 address before allowing the
URL.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 98e3f6be-b5ae-4dc7-812f-31c0e75d8f69

📥 Commits

Reviewing files that changed from the base of the PR and between c8310af and 5c32fb4.

📒 Files selected for processing (8)
  • kits/orderly/README.md
  • kits/orderly/apps/__tests__/diet-rules.test.ts
  • kits/orderly/apps/__tests__/price.test.ts
  • kits/orderly/apps/__tests__/scan-schema.test.ts
  • kits/orderly/apps/components/party-panel.tsx
  • kits/orderly/apps/lib/diet-rules.ts
  • kits/orderly/apps/lib/price.ts
  • kits/orderly/apps/lib/scan-schema.ts

Reversing an earlier decision after checking it against real inputs rather
than arguing from principle.

The two rules agree on every realistic menu string except two, and on every
genuine range the separator is detected so they agree there too. That exposed
the flaw in the previous defence: the "never underestimate the bill" argument
only ever applied to ranges. On "Serves 2 · $18" the maximum returning 18 was
luck, not design, and on "Was $30 now $20" it returned 30, which is wrong.

Ranges still resolve to their upper bound, which is where the safety argument
actually holds. Everywhere else the final number wins, because menu lines put
the price at the end and the leading figures are portions or quantities.

190 tests, 2 new. Typecheck and build clean. Re-verified end to end against
the live flow.

@laxmikhengare laxmikhengare left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third round addressed in c69db3d. All threads now resolved.

190 tests, typecheck clean, production build clean, re-verified end to end against the live deployed flow.

ready for review.

Comment thread kits/orderly/apps/lib/allergen-engine.ts
Comment thread kits/orderly/apps/app/page.tsx
Comment thread kits/orderly/apps/lib/price.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kits/orderly/apps/lib/price.ts`:
- Line 158: The price range parsing must use only the two range endpoints, avoid
treating unspaced fractions as ranges, and cover the reported cases. In
kits/orderly/apps/lib/price.ts:158-158, update RANGE_JOIN to capture both
endpoint tokens and require spacing around "/" while preserving other
separators; in kits/orderly/apps/lib/price.ts:198-200, parse the captured
endpoints and select their upper value instead of all numbers in the line; in
kits/orderly/apps/__tests__/price.test.ts:109-116, add assertions for "1/2
chicken $1.50" and "Serves 20 · $8–$12".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4464d9cb-121d-4741-9333-c29e20b716ed

📥 Commits

Reviewing files that changed from the base of the PR and between 5c32fb4 and c69db3d.

📒 Files selected for processing (3)
  • kits/orderly/README.md
  • kits/orderly/apps/__tests__/price.test.ts
  • kits/orderly/apps/lib/price.ts

Comment thread kits/orderly/apps/lib/price.ts Outdated
My previous fix detected a range and then still took the maximum over every
number on the line, which was only half the job. Two cases were wrong:

  "1/2 chicken $1.50"    parsed as 2     (the fraction, not the price)
  "Serves 20 · $8–$12"   parsed as 20    (the serving count, not the price)

RANGE_JOIN now captures both endpoints and only those are compared, so
unrelated figures on the line cannot become the price. A slash counts as a
separator only when spaced, since menus write half portions as "1/2" and
reading that as a range priced a 1.50 dish at 2.

The earlier "1/2 chicken $24" test passed by coincidence: 24 happened to be
the largest number present. It is kept, alongside a variant where it is not.

192 tests, 2 new. Typecheck and build clean. Re-verified end to end against
the live flow.
@laxmikhengare

Copy link
Copy Markdown
Contributor Author

All CodeRabbit threads resolved. Latest round in aac459e.

192 tests, typecheck clean, production build clean, re-verified end to end against the live deployed flow after each round of changes.

Structural and Studio validation both passing.

Ready for review whenever you have a moment.

@akshatvirmani

Copy link
Copy Markdown
Contributor

/validate

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📡 Running Studio validation — results will appear here shortly.

@akshatvirmani akshatvirmani added the tier-3 Pass label Aug 3, 2026
@akshatvirmani
akshatvirmani merged commit 8b5b00c into Lamatic:main Aug 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants