package main import ( "bufio" "os" "path/filepath" "regexp" "sort" "strings" "git.dev.alexdunmow.com/block/check-safety/internal/helpers" ) type comingSoonViolation struct { file string line int snippet string } var comingSoonAllowedFiles = map[string]bool{ "components/pages/system-pages-list.tsx": true, // Real system page type "components/settings/ai-action-matrix.tsx": true, // AI task for site status copy "components/settings/site-settings-form.tsx": true, // Settings navigation text "components/settings/site-status-section.tsx": true, // Legitimate Site Status feature UI "lib/nav-items.ts": true, // Admin navigation metadata "routes/admin/pages.tsx": true, // System page type enum wiring "lib/api/gen/blockninja/v1/pages_pb.d.ts": true, // Generated proto enum SYSTEM_PAGE_TYPE_COMING_SOON "lib/api/gen/blockninja/v1/settings_pb.d.ts": true, // Generated proto enum SITE_MODE_COMING_SOON } var placeholderCopyPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)\bcoming[\s_-]+soon\b`), regexp.MustCompile(`(?i)\bunder[\s_-]+construction\b`), regexp.MustCompile(`(?i)\bwork[\s_-]+in[\s_-]+progress\b`), regexp.MustCompile(`(?i)\bnot[\s_-]+yet[\s_-]+available\b`), regexp.MustCompile(`(?i)\bavailable[\s_-]+soon\b`), regexp.MustCompile(`(?i)\bcoming[\s_-]+later\b`), } var rePlaceholderComment = regexp.MustCompile(`(?i)\bplaceholder\b`) var rePlaceholderStringLiteral = regexp.MustCompile("`[^`]*\\bplaceholder\\b[^`]*`|\"[^\"]*\\bplaceholder\\b[^\"]*\"|'[^']*\\bplaceholder\\b[^']*'") // isGeneratedProtoOutput reports whether path lives under a `gen/` directory // segment — the buf codegen output tree (e.g. `.../lib/api/gen/blockninja/v1/`). // Scoped to the placeholder/"ship the feature now" check only: generated proto // code is never hand-authored, so its symbols (COMING_SOON enum values, etc.) // must not be flagged as placeholder debt. func isGeneratedProtoOutput(path string) bool { return strings.Contains(filepath.ToSlash(path), "/gen/") } func checkComingSoon(webSrcDir string) []comingSoonViolation { var violations []comingSoonViolation if err := filepath.Walk(webSrcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { if todoDirSkips[info.Name()] { return filepath.SkipDir } return nil } ext := filepath.Ext(path) if !todoFileExts[ext] { return nil } if strings.HasSuffix(path, ".test.ts") || strings.HasSuffix(path, ".test.tsx") { return nil } // Skip buf codegen output (any `gen/` directory segment, e.g. // lib/api/gen/blockninja/v1/*_pb.d.ts). These files are regenerated by // buf and must never be hand-edited; their enum values like // SYSTEM_PAGE_TYPE_COMING_SOON / SITE_MODE_COMING_SOON are real proto // symbols, not authored placeholder debt. if isGeneratedProtoOutput(path) { return nil } relPath, _ := filepath.Rel(webSrcDir, path) if relPath == "" { relPath = path } relPath = filepath.ToSlash(relPath) if comingSoonAllowedFiles[relPath] { return nil } f, openErr := os.Open(path) if openErr != nil { return nil } defer helpers.LogDeferredError(nil, "close coming soon scan file", f.Close, "path", path) scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() trimmed := strings.TrimSpace(line) if isCommentLine(trimmed) || !containsPlaceholderCopy(line) { continue } violations = append(violations, comingSoonViolation{ file: relPath, line: lineNum, snippet: strings.TrimSpace(line), }) } return nil }); err != nil { violations = append(violations, comingSoonViolation{ file: filepath.ToSlash(webSrcDir), snippet: err.Error(), }) } sort.Slice(violations, func(i, j int) bool { if violations[i].file == violations[j].file { return violations[i].line < violations[j].line } return violations[i].file < violations[j].file }) return violations } func checkPlaceholderLanguage(roots []todoScanRoot) []comingSoonViolation { var violations []comingSoonViolation for _, scanRoot := range roots { if scanRoot.root == "" { continue } if err := filepath.Walk(scanRoot.root, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { if todoDirSkips[info.Name()] { return filepath.SkipDir } return nil } ext := filepath.Ext(path) if ext != ".go" && ext != ".tsx" { return nil } if strings.HasSuffix(path, "_test.go") || strings.HasSuffix(path, ".test.tsx") { return nil } slashPath := filepath.ToSlash(path) if strings.Contains(slashPath, "/generated/") || strings.HasSuffix(slashPath, ".pb.go") || strings.HasSuffix(slashPath, ".connect.go") || isGeneratedProtoOutput(path) { return nil } if strings.Contains(filepath.ToSlash(path), "cmd/check-safety/") { return nil } relPath, _ := filepath.Rel(scanRoot.root, path) if relPath == "" { relPath = path } relPath = filepath.ToSlash(relPath) if isAllowedPlaceholderFile(relPath) { return nil } if scanRoot.displayPrefix != "" { relPath = scanRoot.displayPrefix + "/" + relPath } if isAllowedPlaceholderFile(relPath) { return nil } f, openErr := os.Open(path) if openErr != nil { return nil } defer helpers.LogDeferredError(nil, "close placeholder scan file", f.Close, "path", path) scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() trimmed := strings.TrimSpace(line) if !containsPlaceholderLanguage(line, trimmed) { continue } violations = append(violations, comingSoonViolation{ file: relPath, line: lineNum, snippet: trimmed, }) } return nil }); err != nil { violations = append(violations, comingSoonViolation{ file: filepath.ToSlash(scanRoot.root), snippet: err.Error(), }) } } sort.Slice(violations, func(i, j int) bool { if violations[i].file == violations[j].file { return violations[i].line < violations[j].line } return violations[i].file < violations[j].file }) return violations } func containsPlaceholderLanguage(line string, trimmed string) bool { lowerLine := strings.ToLower(line) if strings.Contains(lowerLine, "placeholder=") || strings.Contains(lowerLine, "placeholder:") || strings.Contains(lowerLine, "=placeholder") { return false } if strings.Contains(lowerLine, `.placeholder`) || strings.Contains(lowerLine, `["placeholder"]`) || strings.Contains(lowerLine, `['placeholder']`) || strings.Contains(lowerLine, `json:"placeholder`) || strings.Contains(lowerLine, `data-[placeholder]`) { return false } if isPlaceholderCommentLine(trimmed) && rePlaceholderComment.MatchString(line) { lower := strings.ToLower(trimmed) if strings.Contains(lower, "render a placeholder") || strings.Contains(lower, "renders a placeholder") || strings.Contains(lower, "show placeholder") || strings.Contains(lower, "show empty placeholder") || strings.Contains(lower, "slug placeholder") || strings.Contains(lower, "variable placeholder") || strings.Contains(lower, "placeholder patterns") || strings.Contains(lower, `"placeholder"`) || strings.Contains(lower, "`placeholder`") || strings.Contains(lower, "placeholder with edit button") || strings.Contains(lower, "streaming placeholder") || strings.Contains(lower, "message placeholder") || strings.Contains(lower, "placeholder on error") || strings.Contains(lower, "empty placeholder") || strings.Contains(lower, "placeholder color") || strings.Contains(lower, "placeholder option") { return false } return true } for _, match := range rePlaceholderStringLiteral.FindAllString(line, -1) { literal := strings.TrimSpace(strings.Trim(match, "\"'`")) if literal == "" { continue } if strings.EqualFold(literal, "placeholder") { continue } if strings.HasPrefix(strings.ToLower(literal), "placeholder-") { continue } if strings.Contains(strings.ToLower(literal), "slot-placeholder") { continue } switch strings.ToLower(literal) { case "placeholder color", "placeholder text color": continue } switch strings.ToLower(literal) { case "placeholder content": continue } return true } return false } func containsPlaceholderCopy(line string) bool { for _, pattern := range placeholderCopyPatterns { if pattern.MatchString(line) { return true } } return false } func isPlaceholderCommentLine(trimmed string) bool { return strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "{/*") } func isAllowedPlaceholderFile(relPath string) bool { normalized := strings.TrimPrefix(filepath.ToSlash(relPath), "web/src/") return comingSoonAllowedFiles[normalized] }