Repository navigation
Reject unknown keys in custom-view validation, naming the stray and the near miss (#2733) - #2739
Conversation
…he near miss (#2733) The validator positive-read the keys it knew and defaulted every optional one on absence, so a typo'd key ('filter', 'Filters', 'threshold') validated {valid:true} and stored a syntactically-valid DIFFERENT panel - a dropped filter silently widening the query to the whole fleet. The write path (validate/create/update, web and MCP alike) now rejects any key outside the known set at every level it owns - definition root, composed panel, filters, overlay, notebook cell, variables, range - with a did-you-mean for a near miss. The key universe lives in ComposeSpec beside the parser that reads it, and cannot drift silently in either direction: an every-key acceptance test proves each declared key parses, and the strict check runs BEFORE TryParsePanel on the write path, so a key the parser learns but the set does not is rejected by the new feature's own first write-path test. The shape mismatch that invited the worst mis-shape is named too: run_custom_view_panel's spec nests under 'panel' while a stored panel/cell is flat, and nesting where flat belongs used to fail as the misdirecting "unknown source ''" - the write path and the run path now both say what to unnest, and a missing source reads as missing rather than as an unknown empty catalog name. Strictness is the write path's alone: TryParsePanel stays lenient (with a pin), so a definition stored before the check keeps loading, rendering and running unchanged, and meets the strict check when next edited. v1 read panels keep their open presentation vocabulary (the renderer's contract); their semantically-loaded keys were already individually validated. Every rejection case was proven red against the pre-fix validator: all of them returned {valid:true} (or the misdirecting unknown-source error) before the change. Closes #2733 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
…own-keys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
|
Reviewed the diff (CHANGELOG, DarlingComposeTests.cs, DarlingMcpCustomViewToolsTests.cs, ComposeSpec.cs, DarlingWebEndpoints.cs, DarlingMcpCustomViewTools.cs). Parity: No Lite counterpart exists for custom views/compose/MCP tooling ( Correctness: Traced the new key-universe mechanism end-to-end against the actual parsers rather than trusting the docstrings:
No bugs found. This is a solid, self-verifying change — the "every declared key actually parses" acceptance test plus the write-path-rejects-parser-only-keys ordering genuinely close the drift hole described in the PR body, and I confirmed that guarantee by reading the parsers rather than taking the test's word for it. No missing-index DMV suggestions applicable — this PR doesn't touch T-SQL collectors. |
…-keys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
| if (panel["panel"] is JsonObject) | ||
| { | ||
| return DefinitionValidation.Fail( | ||
| $"panel {i} nests its spec under 'panel' — a stored panel is flat (the {{\"panel\":{{...}}}} wrapper belongs to run_custom_view_panel's spec); put the panel's keys directly on the panel object."); |
There was a problem hiding this comment.
Minor/non-blocking: this "spec nests under 'panel'" wording is now maintained in two independent places — this hardcoded string (v1 arm, panel has neither read nor source) and ComposeSpec.s_panelKeyHints["panel"] (composed-panel arm, panel has a source). Both are correct today (verified against composedPanelToDesc/cellToStored in editor.js/notebook.js and covered by ValidateDefinition_NamesTheRunSpecNesting_OnAPanelCell/_OnADashboardPanel), but a future wording tweak to one is easy to miss in the other. Not asking for a change here, just flagging for awareness.
There was a problem hiding this comment.
Addressed in 26d7c94 — the wording now lives in one internal constant (ComposeSpec.RunSpecNestingHint) feeding both the composed arm's key hint and the v1 arm's message, so a future tweak cannot drift the two apart.
|
Reviewed the diff against CONTRIBUTING.md's C# style guidance and the correctness/parity/security/perf checklist. This is a C#-only change to Darling's custom-view compose/notebook validator — no T-SQL, and no Lite counterpart exists for this feature (grepped for Correctness verification I did by hand, since this is exactly the kind of change where the key-set/parser pairing is easy to get subtly wrong:
No security or performance concerns — this only tightens validation of already-untrusted JSON that was going to be parsed anyway; the Left one non-blocking inline note about a small duplication in the "spec nests under 'panel'" hint text (maintained in two places for the two arms). Otherwise this looks solid — nice self-verifying test suite (red-first against the pre-fix validator). |
…nown-keys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
|
Reviewed the diff (ComposeSpec.cs / DarlingWebEndpoints.cs / DarlingMcpCustomViewTools.cs + tests). This is Darling-only C# work — no T-SQL touched, so the T-SQL style rules do not apply here. Parity: Lite has no custom-view/compose/MCP surface at all (searched for custom_view / ComposeSpec under Lite/, no hits), so there is no Lite/Darling parity drift to flag for this change. Correctness, verified by cross-checking the new key sets against the actual readers (not just the tests):
No correctness, security, or performance issues found. One very minor non-blocking note: NearestKnownKeys tie-break among candidates at equal edit distance depends on HashSet enumeration order, which only affects which key a did-you-mean message names in a tie, never the pass/fail outcome — not worth acting on. |
The 'nests under panel' guidance was worded independently in two places - ComposeSpec's key hint (composed arm) and the v1 arm's hardcoded string - so a future wording tweak to one would silently miss the other. One internal const now feeds both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
|
Reviewed the diff (CHANGELOG.md, Parity: Custom Views (dashboards/notebooks/ Correctness: Traced every new "known key" set back to the parser code it's supposed to mirror, and they line up exactly:
Also checked the write/read split holds: No bugs found. No missing-index-DMV suggestions applicable (none introduced). No security concerns — this only tightens JSON shape validation on an already-gated write surface ( Nice attention to detail on making the every-key acceptance test ( |
A notebook's panel cells all ran against one view-level window, so a
comparison document - two Fridays side by side - was not storable as
live panels. A panel cell may now carry its own 'range', relative
({hours}) or absolute ({windowStart, windowEnd}, ISO-8601 UTC, the
same shape the run path accepts per-run), which the renderer applies
over the view scope for that cell only; absent means inherit the view
window, unchanged.
- ComposeSpec.ParseRange learns the absolute shape (cell-level only:
allowAbsolute), parsing through the run path's own TryParseUtcInstant
(moved into ComposeSpec so the two surfaces share one parser); a
ComposeRange result carries exactly one shape. RangeKeys registers
windowStart/windowEnd for the erikdarlingdata#2739 strict-key walk; the notebook
panel cell's endpoint extras register 'range', and the cell arm
validates it (bad ISO, one-sided pair, both-shapes, span ceiling,
start>=end all reject naming the cell). A view-root absolute range
and a dashboard-panel 'range' are rejected pointing at notebook
cells - the renderer would silently fall back to a default window,
the exact failure this feature closes.
- The renderer pins the cell: compose.js buildRunBody sends the pinned
window (brush-zoom still wins, as it does over hours), and the card
header wears an amber 'Pinned: <window>' badge so pinned-vs-live is
always visible; server scope still follows the scope bar.
- The notebook editor preserves a pin across a save (the desc round
trip used to drop unknown keys, silently unpinning on any edit) and
shows a pin strip with an Unpin affordance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
Closes #2733.
The footgun
The custom-view validator positive-read the keys it knew and defaulted every optional one on absence, so a typo'd key validated
{valid:true}and stored a syntactically-valid different panel than the one the author wrote."filter"for"filters"(or"Filters", case) silently dropped the filter and widened the query to the whole fleet; a droppedoverlay/thresholdschanged what the panel claims. An iterating agent — the MCP surface's whole point — stops atvalid:truewith nobody reading the emitted SQL. Every rejection case in the new tests was proven red against the pre-fix validator first: each one returned{valid:true}(or the misdirectingunknown source '') before the change.What changed
Unknown keys are errors on the WRITE path (validate/create/update — web routes and MCP tools route through the same
ValidateDefinitionauthority), at every level the composed surface owns:kind, panels, variables, rangecellswithout the notebook kindkind, cells, variables, rangepanelsComposeSpec.ComposedPanelKeys(the 13 keysTryParsePanelreads)title/span/hours(+typeon a cell)dimension, op, valuemeasure, aggregate, unitname, dimension, defaulthoursThe error names the offending key and its path, with a did-you-mean for a near miss (case-insensitive match or edit distance ≤ 2):
panel 0: panel has unknown key 'filter' — did you mean 'filters'?The key-universe mechanism. The sets live in
ComposeSpec, beside the parser that reads them, and cannot drift silently in either direction: an every-key acceptance test (ValidateDefinition_AcceptsEveryDeclaredComposedKey_AtOnce) proves each declared key actually parses, and because the strict check runs beforeTryParsePanelon the write path, a key the parser learns but the set does not is rejected by the new feature's own first write-path test — a forgotten registration is loud, not silent.The write/read split (per the issue's fix direction): strictness applies to the write path only.
TryParsePanelitself stays lenient — pinned byTryParsePanel_StaysLenient_ForTheRunAndReadPaths— so a definition stored before the check keeps loading, rendering, and running unchanged, and meets the strict check when someone next edits it. The read/render path never callsValidateDefinition.The panel-nesting shape mismatch the issue flags is kept (unifying it would rewrite the run tool's wire shape and the stored-cell shape for no gain) and explicitly named everywhere it bites:
{"type":"panel","panel":{...}}— the reported repro — now errors with what to unnest instead ofunknown source '';{"panel":{...}}gets the same targeted message in the v1 arm;{"panel":{"panel":{...}}}) gets it on the run path too (safe: no stored panel can carry apanelkey — one never parsed);run_custom_view_panel/validate_custom_viewtool descriptions now state the flat-vs-nested rule.Related error-quality fixes: a panel with no
sourceat all readspanel is missing 'source'(notunknown source ''), and a panel naming neither mode key gets a near-miss suggestion (did you mean 'source' (found 'sorce')?).Deliberately NOT strict: v1 read panels keep their open presentation vocabulary (
rowsKey/xKey/format/emptyText/columns/… — the renderer's contract, spread verbatim from the editor'svizcfg); a server-side enumeration of it would be a second copy that decays, and a stray key there changes pixels, not semantics. The v1 keys with semantic weight were already individually validated (read,viz,span, series colors, and thepathrejection).Verification
{valid:true}, the nesting repro misdirected), green after, via a local net10.0 harness running the real assemblies (the Windows suites can't run on macOS — CI is the arbiter for the xUnit runs).composedPanelToDesc/readPanelToDesc/modelToDefinition, notebook.jsmodelToNotebookDefinition/cellToStored, all five seed notebook templates (mirrored inValidateDefinition_AcceptsEverySeedNotebookTemplate), and the v1 dashboard templates.ValidateDefinition_RejectsACellsDoc_SentWithoutTheNotebookKindnow pins the better targeted message (add"kind":"notebook") instead of "panels must be an array".ComposeCompiler.csandMeasureCatalog.csuntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg