Skip to content

engine(intake): validateIdeaSubmission rejects the IdeaTarget object shape it returns #9609

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

packages/loopover-engine/src/idea-intake.ts exports the discriminated union added by #7635:

export type IdeaTarget =
  | { kind: "existing"; repo: string }
  | { kind: "provision" };

IdeaSubmission.targetRepo is typed as IdeaTarget (line 32), and validateIdeaSubmission returns
{ ok: true, idea: IdeaSubmission } whose targetRepo is a resolved IdeaTarget (line 130).

But the validator's own targetRepo branch (lines 103-108) only recognises two input shapes:

  • a bare "owner/name" string (the back-compat wire form), which it resolves to
    { kind: "existing", repo };
  • an object whose kind is exactly "provision".

Anything else falls through to else errors.push("target_repo_required"). So passing the canonical
{ kind: "existing", repo: "acme/widgets" } — the exact shape the function itself produces, and the
shape any TypeScript caller writing against the exported IdeaSubmission type will construct — is
rejected with target_repo_required.

The round-trip fails: validateIdeaSubmission(validateIdeaSubmission(raw).idea) returns
{ ok: false, errors: ["target_repo_required"] } for every valid existing-repo submission. The
provision variant round-trips correctly, so this is an asymmetry between the two arms of one union,
not a deliberate wire-format restriction.

This is reachable from real callers: src/mcp/server.ts:3731 and src/mcp/server.ts:3747
(loopover_intake_idea / loopover_plan_idea_claims) and src/api/routes.ts both call
validateIdeaSubmission and then thread validated.idea.targetRepo — an IdeaTarget — into
buildClaimPlan, which already accepts both variants (line 285). Only the validator lags.

Requirements

  • validateIdeaSubmission MUST accept { kind: "existing", repo: <string> } as a targetRepo input
    and resolve it to that same IdeaTarget, applying the identical repo format check the bare-string
    branch applies (currently the regex on line 104) and pushing the identical target_repo_malformed
    error code when the repo value fails it.
  • An { kind: "existing" } object whose repo is absent, not a string, or an empty/whitespace-only
    string MUST push target_repo_malformed (not target_repo_required) — it is a recognised-but-invalid
    target, matching how the bare-string branch reports a recognised-but-malformed slug.
  • An object whose kind is neither "existing" nor "provision" MUST keep pushing
    target_repo_required, unchanged.
  • The bare-string and { kind: "provision" } behaviours MUST be byte-for-byte unchanged.
  • No new exported symbol, no new error code beyond the two that already exist.

⚠️ Required pattern: mirror the existing targetRepo branch structure in
validateIdeaSubmission (packages/loopover-engine/src/idea-intake.ts:103-108) — one added
else if arm on the same if/else if/else chain, reusing the same regex constant and the same two
existing error codes. What does NOT satisfy this issue: introducing a separate exported
normalizeIdeaTarget() helper as a second parallel validation surface; loosening the targetRepo
check to accept any object with a repo property; changing the exported IdeaTarget union; or
adding a third error code.

Deliverables

  • validateIdeaSubmission({ id, title, body, targetRepo: { kind: "existing", repo: "acme/widgets" } })
    in packages/loopover-engine/src/idea-intake.ts returns { ok: true } with
    idea.targetRepo deep-equal to { kind: "existing", repo: "acme/widgets" }, asserted by a new
    named case in test/unit/idea-intake-bridge.test.ts.
  • A named regression test asserting the round-trip: feed a validated result's own idea object
    back into validateIdeaSubmission and assert ok === true and that targetRepo is unchanged.
  • { kind: "existing", repo: "not-a-slug" } returns { ok: false, errors: ["target_repo_malformed"] },
    asserted by a new test case.
  • { kind: "existing" } (no repo) and { kind: "existing", repo: "" } each return
    { ok: false, errors: ["target_repo_malformed"] }, asserted by new test cases.
  • { kind: "bogus" } still returns { ok: false, errors: ["target_repo_required"] }, asserted by
    a new test case.
  • Existing bare-string and { kind: "provision" } cases still pass unmodified.

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example adding the { kind: "existing" } accept arm without the two invalid-repo cases, or without
the round-trip regression test — does not resolve this issue.

Test Coverage Requirements

packages/loopover-engine/src/**/*.ts is inside coverage.include in vitest.config.ts (second
entry, right after src/**/*.ts) and carries its own engine Codecov flag. The 99%+ branch-counted
codecov/patch gate applies here exactly as it does to src/**. Every changed conditional needs both
arms exercised: the new kind === "existing" arm must be tested with a valid repo, a malformed
repo, a missing repo, and a non-string repo; the fall-through else must still be tested with an
unrecognised kind. The round-trip test above is the required named regression test for this fix.

Expected Outcome

IdeaSubmission values built against the exported IdeaTarget type validate successfully instead of
failing with target_repo_required, and validateIdeaSubmission is idempotent over its own output for
both union variants rather than only the provision one.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions