Skip to content

fix(validator): warn instead of failing when an RPC option list cannot see a reference value - #66

Merged
Jakub Stok (jakubstokcelonis) merged 2 commits into
mainfrom
fix/tolerate-unresolvable-reference-options
Sep 1, 2026
Merged

Jakub Stok (jakubstokcelonis) merged 2 commits into
mainfrom
fix/tolerate-unresolvable-reference-options

Conversation

@jakubstokcelonis

@jakubstokcelonis Jakub Stok (jakubstokcelonis) commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

https://make.atlassian.net/browse/MAIA-1316

Why

A validator resolving an RPC-backed option store does not necessarily hold every placeholder the store URL interpolates. CallSubscenario's scenario field fetches:

rpc://scenario-service/2.15.6/GetScenarios?teamId={{teamId}}&scenarioId={{scenarioId}}&type=all

A caller without scenarioId asks a narrower question than the picker that offered the value, so a real, active scenario comes back absent and the write is rejected:

Value 'SCN_6074728' not found in options.

…while GetScenarios returned that exact option (active: true) and GetInputInterface accepted the same value. In one production thread the user picked the option from Make's own inline picker and the validator still denied it exists.

Impact (measured, 14 days of production traffic)

  • 652 rejections of well-formed SCN_ values across 195 threads.
  • Those get re-sent identically 73% of the time, against 14% for every other value shape — because the value genuinely is correct, so the model verifies it, finds itself right, and retries. Worst case: 18 identical writes in one thread.
  • For CallSubscenario the blast radius is the whole module: the subscenario's input interface only resolves once parameters.scenario is set, so expect stays empty and every field mapping is rejected too.

What changed

An RPC-backed reference type (scenario, datastore, udt, hook, account, keychain, aiagent, device) whose value is absent from the resolved list now warns instead of erroring — the same treatment allowDynamicValues already gives the mapper domain. Reuses the existing isReferenceType guard; the four duplicated relax predicates are collapsed into one helper.

Scoped deliberately: non-reference selects keep hard validation. A false rejection is unrecoverable for the user; a false acceptance still surfaces as an ordinary setup error in the editor.

Tests

test/scenario.spec.ts — the RPC-backed scenario case now asserts valid: true with a warning (previously a hard error; assertion updated deliberately), plus a new test proving a non-reference select resolved from the same RPC still hard-errors, so the relax cannot silently widen. Verified the first fails against the pre-fix validator and the second passes both ways. Full suite: 479 passed.

Follow-up (not this PR)

Why GetScenarios returns a narrower list when called without scenarioId is server-side and tracked in MAIA-1316 — this change unblocks users but does not answer it.

🤖 Generated with Claude Code

…t see a reference value

A validator resolving an RPC-backed option store does not necessarily hold every
placeholder the store URL interpolates. `CallSubscenario`'s scenario field fetches
`GetScenarios?teamId={{teamId}}&scenarioId={{scenarioId}}`, and a caller without
`scenarioId` asks a narrower question than the picker that offered the value — so a
real, active scenario comes back absent and the write is rejected as
`Value 'SCN_...' not found in options.`

Measured in production over 14 days: 652 such rejections of well-formed `SCN_` values
across 195 threads, re-sent identically 73% of the time (against 14% for every other
value shape) because the value genuinely is correct.

Treat a reference type (scenario, datastore, udt, hook, ...) whose options came from an
RPC the same way `allowDynamicValues` already treats the mapper domain: warn, keep the
value, let the editor surface it as a setup error. A false rejection is unrecoverable
for the user; a false acceptance is not. Non-reference selects keep hard validation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jakubstokcelonis
Jakub Stok (jakubstokcelonis) requested a review from a team as a code owner August 31, 2026 07:44
Copilot AI lite review requested due to automatic review settings August 31, 2026 07:44

Copilot AI 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.

Pull request overview

Adjusts the validation engine so RPC-resolved option lists don’t incorrectly block saves when a reference-type value (e.g. scenario) is missing from the resolved options due to missing interpolation context, aligning behavior with existing “dynamic value” tolerance by emitting a warning instead of an error.

Changes:

  • Introduces unresolvedOptionIsTolerable(...) to centralize the “warn vs error” decision for unresolved RPC options.
  • Extends RPC option-miss tolerance to reference types (via isReferenceType(field.type)), while keeping non-reference selects strict.
  • Updates/extends scenario validation tests to assert the new warning behavior and preserve strictness for ordinary selects.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/validator.ts Centralizes and broadens the RPC “missing option” tolerance to include reference types, switching specific failures from errors to warnings.
test/scenario.spec.ts Updates the scenario RPC case to expect valid: true with a warning, and adds a regression test ensuring non-reference selects still hard-fail.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jakubstokcelonis

Copy link
Copy Markdown
Collaborator Author

Blast-radius measurement (production, 14 days)

Sizing which rejections this actually changes, since the relax covers eight reference types and only scenario was measured in the description. Field identified from the validator's own path in each rejection.

Of 8,862 not found in options rejections:

Bucket Rejections Threads Effect
NON-reference — fallbackModel, spreadsheetId, tableFirstRow, folderId, sheetId, includesHeaders, rowSeparator, … 7,213 3,718 unchanged, still hard-errors
scenario 964 196 now warns
account / connection 595 252 now warns
aiagent 80 24 now warns
datastore / udt 10 8 now warns

81.4% of rejections keep hard validation. The high-volume ones are ordinary RPC-backed selects — the relax keys on the declared field type, not the name — so e.g. rowSeparator rejecting '\n\n' and tableFirstRow are untouched.

The changed buckets are the ones showing the validator-is-wrong signature (identical value re-sent after rejection, i.e. the model compared and found itself right):

  • scenario — 33.3% of distinct values re-sent identically, worst 14×
  • agent — 57.9%, the highest rate in the dataset
  • connection — already has its own escape steer (MAIA-1251) precisely because those values are frequently valid; this makes the validator agree with it

Worth noting fallbackModel (1,379 rejections, the single largest field) is the platform-side empty-option-store defect in MAIA-1296. It is a select, so it stays hard-erroring here — only its reference-typed sibling agent is affected.

Net: the population that could newly pass a genuinely-bad value is ~685 rejections / ~284 threads, concentrated in the fields with the highest identical-resend rates. A false acceptance still surfaces as a setup error in the editor; a false rejection ends the user's session — which is the trade this PR takes deliberately.

@jakubstokcelonis
Jakub Stok (jakubstokcelonis) merged commit cffd183 into main Sep 1, 2026
4 checks passed
@jakubstokcelonis
Jakub Stok (jakubstokcelonis) deleted the fix/tolerate-unresolvable-reference-options branch September 1, 2026 08:09
David Chicaiza (david0723) added a commit that referenced this pull request Sep 2, 2026
Version bump to **2.0.1**.

Includes:
- #69: the inactive branch of a boolean toggle no longer leaks into
`schemas` / `resolvedSchemas`. Those fields came out flat with
`required: true`, and a consumer persisting that list as the module's
resolved form then had the field demanded at run time by validators that
never see the toggle. With `fillDefaults` filling the toggle to `false`,
this was hitting every module that predates the toggle. Validation
outcomes are unchanged.
- #66: an RPC-backed option list that cannot see a reference value warns
instead of failing.

Patch, not minor: no new surface, two fixes. README carries the note.

Once merged, creating the `v2.0.1` GitHub release triggers the npm + JSR
publish workflows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants