feat: Add orderly kit - #316
Conversation
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.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Disabled knowledge base sources:
WalkthroughChangesOrderly 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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
:robot_face: AgentKit Structural ValidationNo contribution files detected in this PR. |
There was a problem hiding this comment.
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 winTwo identical env templates, one source of truth needed.
kits/orderly/apps/.env.exampleandkits/orderly/.env.examplecarry the exact same variable list and comments. Nothing enforces that a future edit to one propagates to the other, andagent.md's quickstart only tells contributors to copy theapps/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 toapps/.env.exampleso 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
⛔ Files ignored due to path filters (2)
kits/orderly/apps/package-lock.jsonis excluded by!**/package-lock.jsonkits/orderly/assets/demo/menu-1.jpgis excluded by!**/*.jpg
📒 Files selected for processing (49)
kits/orderly/.env.examplekits/orderly/.gitignorekits/orderly/README.mdkits/orderly/agent.mdkits/orderly/apps/.env.examplekits/orderly/apps/.gitignorekits/orderly/apps/__tests__/allergen-engine.test.tskits/orderly/apps/__tests__/allergen-table.test.tskits/orderly/apps/__tests__/diet-rules.test.tskits/orderly/apps/__tests__/price.test.tskits/orderly/apps/__tests__/scan-schema.test.tskits/orderly/apps/__tests__/table-solver.test.tskits/orderly/apps/actions/orchestrate.tskits/orderly/apps/app/globals.csskits/orderly/apps/app/layout.tsxkits/orderly/apps/app/page.tsxkits/orderly/apps/components.jsonkits/orderly/apps/components/disclaimer-banner.tsxkits/orderly/apps/components/dish-card.tsxkits/orderly/apps/components/party-panel.tsxkits/orderly/apps/components/table-plan-view.tsxkits/orderly/apps/components/ui/button.tsxkits/orderly/apps/components/ui/input.tsxkits/orderly/apps/components/ui/label.tsxkits/orderly/apps/lib/allergen-engine.tskits/orderly/apps/lib/allergen-table.tskits/orderly/apps/lib/blob-upload.tskits/orderly/apps/lib/diet-rules.tskits/orderly/apps/lib/keyword-match.tskits/orderly/apps/lib/lamatic-client.tskits/orderly/apps/lib/price.tskits/orderly/apps/lib/rate-limit.tskits/orderly/apps/lib/scan-schema.tskits/orderly/apps/lib/table-solver.tskits/orderly/apps/lib/types.tskits/orderly/apps/lib/utils.tskits/orderly/apps/next.config.mjskits/orderly/apps/package.jsonkits/orderly/apps/postcss.config.mjskits/orderly/apps/tsconfig.jsonkits/orderly/constitutions/default.mdkits/orderly/flows/menu-scan.tskits/orderly/lamatic.config.tskits/orderly/model-configs/menu-scan_instructor-llmnode-342_generative-model-name.tskits/orderly/model-configs/menu-scan_multi-modal-llmnode-968_generative-model-name.tskits/orderly/prompts/menu-scan_instructor-llmnode-342_system_0.mdkits/orderly/prompts/menu-scan_instructor-llmnode-342_user_1.mdkits/orderly/prompts/menu-scan_multi-modal-llmnode-968_system_0.mdkits/orderly/prompts/menu-scan_multi-modal-llmnode-968_user_1.md
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.
There was a problem hiding this comment.
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 winMission 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 winAgent, a blank keyword is a live wire wired to every dish on the table.
Trace
patternFor("").normalizedbecomes"", and the compiled pattern is\b(?:e?s)?\b(?!\s+(?:free)\b). The optional group can match zero-width, so both\bassertions collapse onto the same position. That position exists at the start of almost any non-empty haystack that is not immediately followed by"free ".normalizedTextHasKeywordonly guardshaystack === ""; it does not guard an emptykeyword.This is not hypothetical.
findKeywordInAnyis called withdiner.dislikesas 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
📒 Files selected for processing (12)
kits/orderly/README.mdkits/orderly/apps/__tests__/allergen-table.test.tskits/orderly/apps/__tests__/scan-schema.test.tskits/orderly/apps/__tests__/table-solver.test.tskits/orderly/apps/app/page.tsxkits/orderly/apps/components/party-panel.tsxkits/orderly/apps/lib/allergen-engine.tskits/orderly/apps/lib/allergen-table.tskits/orderly/apps/lib/diet-rules.tskits/orderly/apps/lib/keyword-match.tskits/orderly/apps/lib/scan-schema.tskits/orderly/apps/lib/table-solver.ts
|
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 FixedSecurity
Correctness
Completeness
Performance
Clarity
Declined, with reasonsreact-hook-form refactor of Restricting Pass 1 to Rennet detection for halal. The suggestion references a
Last parsed value instead of Root Happy to revisit any of the declined items if you disagree. Requesting |
|
📡 Running Studio validation — results will appear here shortly. |
Studio Runtime Validation (Phase 2)✅ Studio validation passed. The kit loaded successfully in Lamatic Studio. This PR is ready for final review and merge. |
|
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:
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.
There was a problem hiding this comment.
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 winMission: replace the hand-rolled IPv6 gate with an audited parser.
The
::ffff:...bypass is closed, butembeddedIPv4still makes a security-critical SSRF decision from string slicing. This path also leaves deprecated IPv4-compatible literals likehttps://[::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 asip-addressoripaddr.jsto 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
📒 Files selected for processing (8)
kits/orderly/README.mdkits/orderly/apps/__tests__/diet-rules.test.tskits/orderly/apps/__tests__/price.test.tskits/orderly/apps/__tests__/scan-schema.test.tskits/orderly/apps/components/party-panel.tsxkits/orderly/apps/lib/diet-rules.tskits/orderly/apps/lib/price.tskits/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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
kits/orderly/README.mdkits/orderly/apps/__tests__/price.test.tskits/orderly/apps/lib/price.ts
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.
|
All CodeRabbit threads resolved. Latest round in 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. |
|
/validate |
|
📡 Running Studio validation — results will appear here shortly. |
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
It does not guarantee optimality. The selection is greedy set cover with one improvement pass. The README says so rather than overclaiming.
Tradeoffs
codeNode, so it can be unit-tested. For a safety tool, testability beats flow portability.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.
menu-scanflow with trigger, multimodal LLM, Instructor LLM, structured response, and API response mapping nodes.