HOMELAB-1532: fix(plane): fail loudly on unknown project prefix #12

Merged
aaron merged 1 commit from plane/HOMELAB-1532-unknown-prefix-fail-loud into main 2026-09-10 18:53:41 +00:00
Owner

What

parseTicketIdentifier() returned null for two unrelated reasons, and all 7 call sites treated null as "this must be a raw UUID":

const parsed = parseTicketIdentifier(ref, currentProject);
if (parsed) { ...resolve by prefix... }
else {
  workItemId = ref;              // "SENDR-5" sent to the API as a work-item UUID
  projectId  = resolveProject(); // silently HOMELAB
}

The two null cases:

  1. Not an identifier at all (UUID / free text) → treating it as a raw id is correct.
  2. Well-formed PREFIX-N with an unknown prefix (SENDR-5, or SEND-49 before HOMELAB-1531) → treating it as a UUID is wrong. The literal string goes to the API as a UUID against HOMELAB, producing an opaque 400/404 far from the root cause.

Read paths fail confusingly; write paths (plane_update_item, plane_comment, plane_create_relation) attempt writes against a bogus UUID.

Changes

  • parseTicketIdentifier — throws a new UnknownProjectPrefixError when the input is PREFIX-N but the prefix is not in PROJECT_MAP, listing the available prefixes (message style matches the existing resolveProject(explicit) error). Still returns null only for genuine non-identifiers, so the UUID path and bare-number default are unchanged.
  • resolveProject() (no-arg) — silently fell back via projectMap[currentProject] ?? projectId; now throws. The explicit-arg path already threw.
  • resolveProjectId() deleted — dead code (zero call sites, not imported by index.ts) whose docstring advertised the same dangerous "Falls back to HOMELAB" behaviour. Deleting it denies future callers the landmine, which is the point of this change.

Why a throw rather than a return value

The throw lives inside the one function every call site already funnels through, so all 7 are fixed without touching them — and any future call site inherits the protection automatically. That is the actual goal ("no issues with plane in the future"), not just patching today's callers.

Verification

13/13 cases:

known prefixes still resolve
  ✔ "SEND-49" resolves            ✔ "homelab-42" (lowercase) resolves
  ✔ "INTL-8" resolves             ✔ "APEX-5" resolves
bare number: ambient/default prefix
  ✔ "42" -> HOMELAB by default    ✔ "42" -> SEND with ambient SEND
  ✔ " 42 " whitespace tolerated
UUID path must stay null (not throw)
  ✔ uuid -> null                  ✔ free text -> null
  ✔ "2026-09-10" -> null (date, not an id)
unknown prefix must THROW, not return null
  ✔ "SENDR-5" throws (correct type + prefix + available list)
  ✔ "FOO-1" throws (not treated as uuid)
  ✔ message is actionable

Exact caller shape from index.ts:

SEND-49   → {"via":"identifier","prefix":"SEND","seq":49}
SENDR-5   → THROWS: Unknown project prefix "SENDR". Available: HOMELAB, INTL, GIQ, APEX, SEND
FOO-1     → THROWS: Unknown project prefix "FOO". Available: HOMELAB, INTL, GIQ, APEX, SEND
42        → {"via":"identifier","prefix":"HOMELAB","seq":42}
<uuid>    → {"via":"raw-uuid",...}          ← UUID path preserved

Blast-radius check

  • The sidebar bridge fetchTicket() can only receive identifiers for projects the workspace actually returns — and every Plane project is already in PROJECT_MAP (verified against the API: APEX, GIQ, HOMELAB, INTL, SEND). So no legitimate sidebar path reaches the throw.
  • Even if one ever did, PlaneSidebar already wraps fetchTicket in try/catch and renders e.message.
  • HOMELAB-1531 (#11) added the missing SEND data. This fixes the behaviour, so the next unknown prefix fails loudly instead of silently resolving to the wrong project.
## What `parseTicketIdentifier()` returned `null` for **two unrelated reasons**, and all 7 call sites treated `null` as "this must be a raw UUID": ```ts const parsed = parseTicketIdentifier(ref, currentProject); if (parsed) { ...resolve by prefix... } else { workItemId = ref; // "SENDR-5" sent to the API as a work-item UUID projectId = resolveProject(); // silently HOMELAB } ``` The two `null` cases: 1. **Not an identifier at all** (UUID / free text) → treating it as a raw id is correct. 2. **Well-formed `PREFIX-N` with an unknown prefix** (`SENDR-5`, or `SEND-49` before HOMELAB-1531) → treating it as a UUID is **wrong**. The literal string goes to the API as a UUID against HOMELAB, producing an opaque 400/404 far from the root cause. Read paths fail confusingly; **write paths** (`plane_update_item`, `plane_comment`, `plane_create_relation`) attempt writes against a bogus UUID. ## Changes - **`parseTicketIdentifier`** — throws a new `UnknownProjectPrefixError` when the input is `PREFIX-N` but the prefix is not in `PROJECT_MAP`, listing the available prefixes (message style matches the existing `resolveProject(explicit)` error). Still returns `null` **only** for genuine non-identifiers, so the UUID path and bare-number default are unchanged. - **`resolveProject()`** (no-arg) — silently fell back via `projectMap[currentProject] ?? projectId`; now throws. The explicit-arg path already threw. - **`resolveProjectId()` deleted** — dead code (zero call sites, not imported by `index.ts`) whose docstring advertised the same dangerous *"Falls back to HOMELAB"* behaviour. Deleting it denies future callers the landmine, which is the point of this change. ## Why a throw rather than a return value The throw lives inside the one function every call site already funnels through, so all 7 are fixed without touching them — and any **future** call site inherits the protection automatically. That is the actual goal ("no issues with plane in the future"), not just patching today's callers. ## Verification 13/13 cases: ``` known prefixes still resolve ✔ "SEND-49" resolves ✔ "homelab-42" (lowercase) resolves ✔ "INTL-8" resolves ✔ "APEX-5" resolves bare number: ambient/default prefix ✔ "42" -> HOMELAB by default ✔ "42" -> SEND with ambient SEND ✔ " 42 " whitespace tolerated UUID path must stay null (not throw) ✔ uuid -> null ✔ free text -> null ✔ "2026-09-10" -> null (date, not an id) unknown prefix must THROW, not return null ✔ "SENDR-5" throws (correct type + prefix + available list) ✔ "FOO-1" throws (not treated as uuid) ✔ message is actionable ``` Exact caller shape from `index.ts`: ``` SEND-49 → {"via":"identifier","prefix":"SEND","seq":49} SENDR-5 → THROWS: Unknown project prefix "SENDR". Available: HOMELAB, INTL, GIQ, APEX, SEND FOO-1 → THROWS: Unknown project prefix "FOO". Available: HOMELAB, INTL, GIQ, APEX, SEND 42 → {"via":"identifier","prefix":"HOMELAB","seq":42} <uuid> → {"via":"raw-uuid",...} ← UUID path preserved ``` ## Blast-radius check - The sidebar bridge `fetchTicket()` can only receive identifiers for projects the workspace actually returns — and **every** Plane project is already in `PROJECT_MAP` (verified against the API: `APEX, GIQ, HOMELAB, INTL, SEND`). So no legitimate sidebar path reaches the throw. - Even if one ever did, `PlaneSidebar` already wraps `fetchTicket` in `try/catch` and renders `e.message`. ## Related - HOMELAB-1531 (#11) added the missing `SEND` **data**. This fixes the **behaviour**, so the next unknown prefix fails loudly instead of silently resolving to the wrong project.
parseTicketIdentifier() returned null for two unrelated reasons — "not an
identifier at all" (a UUID) and "well-formed PREFIX-N with an unknown
prefix" — and all 7 call sites treated null as "this must be a raw UUID":

    const parsed = parseTicketIdentifier(ref, currentProject);
    if (parsed) { ...resolve by prefix... }
    else { workItemId = ref; projectId = resolveProject(); }

So plane_get_item("SENDR-5") sent the literal string "SENDR-5" to the API
as a work-item UUID against the HOMELAB project, yielding an opaque 400/404
far from the root cause. Write paths (plane_update_item, plane_comment,
plane_create_relation) did the same.

Changes:
- parseTicketIdentifier: throws UnknownProjectPrefixError when the input is
  PREFIX-N but the prefix is not in PROJECT_MAP, listing available prefixes.
  Still returns null ONLY for genuine non-identifiers, so the UUID path and
  the bare-number default are unchanged.
- resolveProject(): the no-arg path silently fell back to HOMELAB
  (projectMap[currentProject] ?? projectId); it now throws instead. The
  explicit-arg path already threw.
- Delete resolveProjectId(): dead code (zero call sites, not imported) whose
  docstring advertised the same dangerous "Falls back to HOMELAB" behaviour.
  Removing it denies future callers the landmine.

Verified: 13/13 unit cases (known prefixes, lowercase, whitespace, bare
number + ambient, UUID/free-text/date still null, unknown prefix throws with
an actionable message) plus a caller-shape simulation.
aaron merged commit 732d556988 into main 2026-09-10 18:53:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/pi-extensions!12
No description provided.