{"record":{"id":"16e432500e6d59fd","repo":"gastownhall/beads","slug":"read-rule-file-s-w","errorCode":null,"errorMessage":"read rule file %s: %w","messagePattern":"read rule file (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/rules.go","lineNumber":104,"sourceCode":"\t\"suppress\": {\"log\"},\n}\n\n// --- Regex Patterns ---\n\nvar (\n\theadingRe = regexp.MustCompile(`(?m)^#\\s+(.+)`)\n\tdoRe      = regexp.MustCompile(`(?i)^\\*\\*Do:?\\*\\*:?\\s*(.*)`)\n\tdontRe    = regexp.MustCompile(`(?i)^\\*\\*Don'?t:?\\*\\*:?\\s*(.*)`)\n)\n\n// --- Core Functions ---\n\n// ParseRuleFile reads a .md file and extracts structured rule data.\nfunc ParseRuleFile(path string) (RuleFile, error) {\n\t// #nosec G304 -- path comes from controlled filepath.Join of user-specified rules directory\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn RuleFile{}, fmt.Errorf(\"read rule file %s: %w\", path, err)\n\t}\n\n\tcontent := string(data)\n\tname := strings.TrimSuffix(filepath.Base(path), \".md\")\n\n\trf := RuleFile{\n\t\tPath: path,\n\t\tName: name,\n\t\tBody: content,\n\t\t// Rough token estimate: 1 token ~ 4 chars\n\t\tTokens: len(content) / 4,\n\t}\n\n\t// Extract title from first heading\n\tif m := headingRe.FindStringSubmatch(content); len(m) > 1 {\n\t\trf.Title = strings.TrimSpace(m[1])\n\t} else {\n\t\trf.Title = name","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/rules.go#L86-L122","documentation":"ParseRuleFile wraps any os.ReadFile failure with the path of the .md rule file it attempted to read. It is thrown when the file cannot be opened or read (missing file, permission denied, path is a directory). The %w wrap preserves the underlying os error for errors.Is/As inspection.","triggerScenarios":"Calling ParseRuleFile (directly or via RunAudit/runRulesCompact) with a path that does not exist, is unreadable, or points to a directory instead of a .md file.","commonSituations":"Misconfigured rules directory passed to `bd rules audit`/`bd rules compact`; a rules file was renamed or deleted; symlink pointing at a missing target; filesystem permission issues; passing a directory path instead of a file.","solutions":["Verify the file exists and is readable: ls -l <path> (or stat <path>).","Check that the rules directory configured for bd contains the expected .md files.","Fix filesystem permissions on the rules directory/file.","If the path should be optional, handle the wrapped os.IsNotExist(err) before treating it as fatal."],"exampleFix":"// before\nrf, err := ParseRuleFile(maybePath)\nif err != nil { return err }\n// after\nif _, statErr := os.Stat(maybePath); os.IsNotExist(statErr) {\n    return nil // skip missing rule file\n}\nrf, err := ParseRuleFile(maybePath)\nif err != nil { return err }","handlingStrategy":"try-catch","validationCode":"if _, err := os.Stat(path); err != nil {\n    // skip or report before calling ParseRuleFile\n}","typeGuard":null,"tryCatchPattern":"rf, err := ParseRuleFile(path)\nif err != nil {\n    if errors.Is(err, os.ErrNotExist) { /* handle missing file */ }\n    return fmt.Errorf(\"rules audit: %w\", err)\n}","preventionTips":["Stat the file before parsing when existence is optional.","Keep rules .md files under a single versioned directory so paths are stable.","Run bd as a user with read access to the rules directory.","Never pass directory paths to ParseRuleFile."],"tags":["filesystem","io","rules","error-wrapping"],"backgroundTag":"file-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}