{"record":{"id":"e5e31d0b388ac5db","repo":"gastownhall/beads","slug":"w-issue-s-e5e31d","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/commenter.go","lineNumber":80,"sourceCode":"// refusing an id that names neither an issue nor a wisp.\n//\n// The existence probe is here rather than left to the insert's own so the\n// refusal is TYPED: AddIssueCommentInTx reports a missing anchor as prose, and\n// a caller of this role classifies with errors.Is. It resolves the plane in\n// the same transaction the insert runs in, so a comment cannot land on a row\n// an earlier read saw and this one did not.\n//\n//nolint:gosec // G201: issueTable comes from WispTableRouting (\"issues\" or \"wisps\")\nfunc resolveCommentPlaneInTx(ctx context.Context, tx *sql.Tx, issueID string) (string, error) {\n\tisWisp := IsActiveWispInTx(ctx, tx, issueID)\n\tissueTable, _, _, _ := WispTableRouting(isWisp)\n\tvar exists bool\n\tif err := tx.QueryRowContext(ctx,\n\t\tfmt.Sprintf(`SELECT EXISTS(SELECT 1 FROM %s WHERE id = ?)`, issueTable), issueID).Scan(&exists); err != nil {\n\t\treturn \"\", fmt.Errorf(\"check issue existence: %w\", err)\n\t}\n\tif !exists {\n\t\treturn \"\", fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, issueID)\n\t}\n\tif isWisp {\n\t\treturn \"wisp_comments\", nil\n\t}\n\treturn \"comments\", nil\n}\n","sourceCodeStart":62,"sourceCodeEnd":87,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/commenter.go#L62-L87","documentation":"resolveCommentPlaneInTx refuses to add a comment when the IssueID names neither an existing issue nor an active wisp in the same transaction. The refusal is deliberately typed — storage.ErrNotFound wrapped as \"%w: issue %s\" — so callers can classify with errors.Is instead of parsing prose from the insert itself. The existence probe runs in the caller's transaction to prevent a TOCTOU race where the anchor is deleted between the check and the insert.","triggerScenarios":"Calling AddComment with a typo'd or deleted issue ID (bd comment bd-9999 \"x\"); commenting on an issue another process closed/deleted moments earlier; using an ID from a stale local export after a sync removed the issue.","commonSituations":"Automation following issue IDs scraped from logs or old tickets; scripts with hardcoded IDs after a database reset (bd init in a fresh clone); race between a cleanup job deleting wisps and a bot adding comments.","solutions":["Verify the issue ID with bd show <id> (or an equivalent lookup) before commenting; fix typos in the ID.","Handle errors.Is(err, storage.ErrNotFound) explicitly and skip/report the missing anchor rather than retrying.","Re-sync or re-export your local data if the issue legitimately exists upstream but not locally."],"exampleFix":"// before\nif err := store.AddComment(ctx, req); err != nil {\n\treturn err // blind failure\n}\n\n// after\nif err := store.AddComment(ctx, req); err != nil {\n\tif errors.Is(err, storage.ErrNotFound) {\n\t\tlog.Warnf(\"issue %s gone, skipping comment\", req.IssueID)\n\t\treturn nil\n\t}\n\treturn err\n}","handlingStrategy":"try-catch","validationCode":"// pre-check (best effort; the typed error is authoritative)\nif _, err := store.GetIssue(ctx, issueID); err != nil {\n\treturn fmt.Errorf(\"cannot comment: %w\", err)\n}","typeGuard":"func isMissingIssue(err error) bool {\n\treturn errors.Is(err, storage.ErrNotFound)\n}","tryCatchPattern":"if err := store.AddComment(ctx, req); err != nil {\n\tif errors.Is(err, storage.ErrNotFound) {\n\t\tlog.Warnf(\"issue %s not found; skipping comment\", req.IssueID)\n\t\treturn nil\n\t}\n\treturn err\n}","preventionTips":["Resolve issue IDs from bd's own listing rather than free-text/log scraping.","Handle ErrNotFound as an expected outcome in automation (skip, don't crash).","Re-sync local data before batch operations that reference upstream IDs."],"tags":["not-found","comments","go"],"backgroundTag":"issue-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}