Skip to content

aggregator: yes/no survey answers are never validated — any string (or even an array) is accepted as a yes/no answer #1486

Closed

Description

@dezren39

aggregator: yes/no survey answers are never validated — any string (or even an array) is accepted as a yes/no answer

Summary

The Needs-you survey's POST-boundary check (checkActionAgainstItem) promises that survey answers are checked against the published question's options — and it works for single and multi questions. For yesno questions it never fires: any value is accepted and stored as the answer to a yes/no question, including nonsense strings and even an array of both options. The card then renders that value as "✓ answered: banana".

Evidence

Three pieces line up into the hole:

  • The sanitizer only attaches options to single/multi questions — sanitizeQuestions in pm-stack/aggregator/src/tier-items.ts:117-134. A yesno question's options are dropped even when the publisher supplies them (verified: a yesno question published with options: [{id:"yes"},{id:"no"}] sanitizes to a question with no options field).
  • The boundary check validates a yesno answer only when options exist — checkActionAgainstItem, tier-items.ts:347: if ((q.type === "single" || q.type === "yesno") && q.options) { ... }. Since yesno questions never carry options after sanitization, the guard is dead code for them.
  • The renderer knows the real option set — questionFieldHtml in pm-stack/aggregator/src/render.ts:963-968 synthesizes [{id:"yes",label:"Yes"},{id:"no",label:"No"}] when q.options is absent. The UI can only ever submit yes/no; the API accepts everything else anyway.

Verified this run on main b27dbe8e with the verbatim tier-items.ts (sha256 cdb1fd21…, identical on sandbox and laptop) under Bun on the laptop (~/git/fleet/probe-2026-10-27-laneB-yesno; log hidden_files/probe-2026-10-27-laneB-yesno/out.log):

control single bogus value        : REJECTED: answer for "audience" must be one of its options
control single valid value         : ACCEPTED (null)
control multi string (not array)   : REJECTED: answers for "tags" must be chosen from its options
control multi bogus entry          : REJECTED: answers for "tags" must be chosen from its options
yesno valid 'yes'                  : ACCEPTED (null)
yesno BOGUS 'banana'               : ACCEPTED (null)   <-- bug
yesno BOGUS array ['yes','no']     : ACCEPTED (null)   <-- bug
yesno-with-opts BOGUS 'banana'     : ACCEPTED (null)   <-- bug (sanitizer stripped the options)
text BOGUS array ['a','b']         : ACCEPTED (null)   <-- same shape: no per-type value-shape check for text either

Downstream, the bogus value is merged into the card state verbatim (card-state.ts answered map, latest answer wins) and printed as the answered value (render.ts question state line), and it is what the consumer of the answer action receives — a yes/no answer it cannot interpret.

Why it matters

The boundary check is the contract the consumers rely on (#1291/#1293 design: "option and survey membership … checks"). Every other question type is enforced; yes/no is the most common triage question shape ("Go ahead?"), and its answers silently escape validation, so a malformed or hand-crafted POST can record an unanswerable "answer" that marks a required question as answered on the card.

Acceptance criteria

  • A yesno answer whose value is not exactly "yes" or "no" (or one of the question's published option ids, if the sanitizer is changed to keep publisher-supplied yesno options) is rejected by checkActionAgainstItem with a legible error, in the same style as the single case.
  • A non-string value (e.g. an array) for a yesno or single question is rejected; an array value for a text question is rejected (per-type value shape: single/yesno/text → string, multi → string array).
  • Valid answers still pass: yes/no for yesno, a published option for single, a string for text, an option-id array for multi; partial submits (feat(dashboard): multi-question cards — render every question, submit one answers array #1451) are unaffected.
  • Regression tests in pm-stack/aggregator/test/needs-user.test.ts cover the yesno bogus-value and array cases and the text array case, alongside the existing single/multi controls.

Activity

  1. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane B, new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b, 2026-10-27 13:10 CDT run
    • Found via: filed this run
    • Task: Triage the yes/no survey-answer validation gap: sanitizer strips yesno options (tier-items.ts:117-134), the boundary check's yesno arm is therefore dead (tier-items.ts:347), and the renderer synthesizes the yes/no option set the check never uses (render.ts:963-968). Verified with the verbatim tier-items.ts under Bun on the laptop — bogus and array yesno answers accepted, single/multi controls correctly rejected.
    • Expectation: effort/s — done when checkActionAgainstItem rejects non-yes/no and wrong-shape answers per question type, with regression tests in needs-user.test.ts
    • Links: probe log in the goal bookkeeping (hidden_files/probe-2026-10-27-laneB-yesno/out.log); queue packet enqueued to repo-health-code-pilot (triage)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 7 remaining items

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (small_fix)

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenChamber scheduled lane · 2026-11-01 · task 4772
    • Task: Validate yes/no survey answers at the POST boundary per queue packet; fresh issue state is open and no matching PR was found.
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off (PR opened)

  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (PR opened)

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenChamber scheduled lane · 2026-11-01 · task 4772
    • Result: PR fix(aggregator): validate survey answers by question type #3742 is open; queue task completed as pr_opened.
    • Verification: targeted needs-user tests pass (37). Full suite and typecheck have unrelated failures reproduced on clean main; two repository workflow runs failed before starting jobs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:in-reviewtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions