Repository navigation
Description
grist(update-choice-options): a non-list choices value is silently shredded or crashes the tool — a string choice becomes one-character choices and the read-back verification passes the corruption
Evidence (verified this run, laptop @ 9123103 == origin/main)
pm-stack/grist/update-choice-options.py extends a Grist Choice column's options. It reads the column's widgetOptions JSON, merges additions, PATCHes the result back, and verifies by read-back. Nothing checks the shape of what came back:
merge_choices(:45) doeschoices = list(merged.get("choices") or [])at :53. If the storedchoicesis a string,list()shreds it into characters. If it is a list containing non-strings, they pass through unchecked.get_widget_options(:90) returnsjson.loads(raw)at :95 verbatim — if the storedwidgetOptionsJSON parses to a non-object (a string, a list, a number), that value flows straight intomerge_choices, wheredict(widget_options)raises a rawValueError.run()(:165) catches onlyTimeoutError, so the tool dies with a traceback instead of its documented clean exit codes (0 ok / 1 API-or-verification failure / 2 usage).- The read-back verification in
main(:147) compares read-back choices against the merged value — so a corrupted merge verifies as good. It never compares against the original choices' shape.
Probe (laptop, ~/git/fleet/probe-grist-choices/probe.py, real module imported from the primary tree):
- Control:
merge_choices({"choices": ["code", "infra"]}, ["id"])→["code", "infra", "id"]✔ merge_choices({"choices": "code"}, ["id"])→['c', 'o', 'd', 'e', 'id']— the existing choicecodeis destroyed and replaced with four one-character choices;mainwould PATCH this and the read-back check at :147 would pass it.merge_choices(json.loads('"justastring"'), ["x"])→ uncaughtValueError: dictionary update sequence element #0 has length 1; 2 is required(traceback out ofrun(), which catches onlyTimeoutError).merge_choices({"choices": [1, None, True]}, ["id"])→[1, None, True, "id"]— non-string choices pass through and would be written back as JSON1 / null / truechoices.
Why it matters
This tool is the supported path for extending Choice columns (#1287): it writes to a live Grist doc and its only safety net is the read-back verification — which compares against the possibly-corrupted merge, not the original data. A widgetOptions value whose choices was written by anything else (a Grist UI change, a manual edit, an older/newer Grist version's serialization, a ChoiceList vs Choice confusion) in a non-list shape is silently destroyed on the next routine --add, with the tool reporting success. The non-object case separately breaks the tool's exit-code contract with a raw traceback.
Acceptance criteria
merge_choices(or its caller) refuses awidgetOptionsthat is not a JSON object, and achoicesvalue that is not a list of strings, with a clear named error — never a shredded/coerced write and never a raw traceback.- The refusal happens before any PATCH is sent; the stored column is untouched on a shape failure.
- The CLI surfaces shape failures through its documented exit-code contract (a clean message on stderr and a non-zero exit, like the timeout path in
run()). - Tests in
pm-stack/grist/test_update_choice_options.pypin: stringchoices, dictchoices, non-objectwidgetOptions, and a list containing non-string entries — plus the existing controls (normal append, idempotence, empty options, duplicate additions, input not mutated) unchanged. python3 -m unittest test_update_choice_optionsgreen inpm-stack/grist(laptop or CI).
Activity
developing-today commentedon Nov 6, 2026
CODE-2006
dezren39 commentedon Nov 6, 2026
🤖 Agent note · filed
new-issue-creator-code-bnew-issue-creator-code-b, run 2026-11-06 11:50 CSTchoicesshape defect in pm-stack/grist/update-choice-options.py — a string choices value is shredded by list() and PATCHed back with read-back verification passing, and a non-object widgetOptions crashes with a raw ValueError tracebackdezren39 commentedon Nov 6, 2026
🤖 Agent note · triage: CONFIRMED
direct, so this lane verified it.~/git/code@0819afd0fc5a987b1602ab880f204908a2183f08, real module imported):merge_choices({"choices": ["code", "infra"]}, ["id"])→["code", "infra", "id"]✔merge_choices({"choices": "code"}, ["id"])→['c', 'o', 'd', 'e', 'id']— shredding reproduced exactly as filed.merge_choices(json.loads('"justastring"'), ["x"])→ uncaughtValueError: dictionary update sequence element #0 has length 1; 2 is required(run()catches onlyTimeoutError).merge_choices({"choices": [1, None, True]}, ["id"])→[1, None, True, "id"]— pass through unchecked.merge_choices({"choices": {"a": 1}}, ["id"])→['a', 'id']— a dict is silently replaced by its keys. Same defect class; the routed fix's tests already pin the dict case.list(merged.get("choices") or [])atpm-stack/grist/update-choice-options.py:53;get_widget_optionsreturnsjson.loads(raw)verbatim; read-back inmaincompares against the merged value.repo-health:v1:fix:developing-today/code:issue:6588) carrying issue grist(update-choice-options): a non-list choices value is silently shredded or crashes the tool — a string choice becomes one-character choices and the read-back verification passes the corruption #6588's acceptance criteria verbatim. Task 11318 completes astriaged.23 remaining items
dezren39 commentedon Nov 7, 2026
🤖 Agent note · picked up
fleet queue healthHTTP 503 PGRST002, exit 4; outage tracked as issue infra: repo-queue PostgREST timeout: global failures lane-visible again — recurrence after #6450 closed (2026-11-06 21:15-21:31Z) #6619). Fallback claim on issue grist(update-choice-options): a non-list choices value is silently shredded or crashes the tool — a string choice becomes one-character choices and the read-back verification passes the corruption #6588; lane code-p released it free earlier and this lane claimed it fresh after that release.pm-stack/grist/update-choice-options.py: refuse a non-objectwidgetOptionsand a non-list-of-stringschoiceswith a named error before any PATCH, surfaced through the documented exit-code contract (clean stderr, exit 1 viarun()), with tests pinning the probe cases from this issue.pm-stack/grist/test_update_choice_options.py, branch + PR, merge under the standing criteria if verified.queue/fallback-6588-grist-choices-shape, laptop worktree~/git/fleet/fb-6588dezren39 commentedon Nov 7, 2026
Progress from
repo-worker-code-i: picked up: script and existing tests read on current main; fix = WidgetOptionsError shape validation in merge_choices + get_widget_options, run() maps it to exit 1 before any PATCH; next: laptop worktree + implementationdezren39 commentedon Nov 7, 2026
Progress from
repo-worker-code-i: pr_opened: PR for issue #6588 opened (branch queue/fallback-6588-grist-choices-shape, commit fc49b8d); unittest 15/15 green on laptop, pre-fix suite fails (1 failure + 8 errors); next: PR checks + Codex, then merge under standing criteriaMerge pull request #6719 from developing-today/queue/fallback-6588-gr…
dezren39 commentedon Nov 7, 2026
🤖 Agent note · done
pm-stack/grist/update-choice-options.pyso a malformed storedwidgetOptions/choicesis refused by name before any PATCH.fixed_merged— PR grist(update-choice-options): refuse widgetOptions/choices shape failures by name (#6588) #6719 merged (merge commit ae31c5d); this issue auto-closed by the merge.check_widget_options+WidgetOptionsError: storedwidgetOptionsmust be a JSON object andchoices(when present, non-null) a list of strings;merge_choicesandget_widget_optionsboth validate (invalid JSON included), so refusal happens before any PATCH is built and the stored column is untouched on a shape failure.run()maps the error to a clean stderr message + exit 1, matching the documented exit-code contract (the timeout path's shape). Null/absentchoiceskeep their existing no-choices meaning.python3 -m unittest test_update_choice_options— 15/15 green on the laptop, including new merge-level cases (string/dict choices, non-object widgetOptions, non-string entries) and CLI-level cases pinning exit 1 with zero PATCHes sent. Against the pre-fix script the suite fails (1 failure + 8 errors) and prints the shredding it prevents (code -> ['c', 'o', 'd', 'e', 'id']).queue/fallback-6588-grist-choices-shapedezren39 commentedon Nov 7, 2026
Released by
repo-worker-code-i(outcome: done). Outcome: fixed_merged. PR #6719 merged (merge commit ae31c5d); issue #6588 auto-closed. update-choice-options.py now validates stored widgetOptions/choices shape via check_widget_options/WidgetOptionsError before any PATCH (string choices refused, not shredded; non-object and invalid-JSON refused by name), surfaced as clean stderr + exit 1 via run(). Tests 15/15 green on laptop; pre-fix suite fails (1 failure + 8 errors). Codex zero findings on PR #6719.