From 830f0c7208f94521a59dfe6f96d496509e6b29e9 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Thu, 23 Jul 2026 14:43:45 +0100 Subject: [PATCH] fix(oo): skip junk dirs in applications Discover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop inventing CRM companies from node_modules/tools README trees. Only sync dossier-like folders (source-NNN-…) and invent Company from the slug token after the numeric id. --- README.md | 6 +++- cmd/oo/applications/applications.go | 40 ++++++++++++++++++++-- cmd/oo/applications/applications_test.go | 43 ++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 80b7896..91bc938 100644 --- a/README.md +++ b/README.md @@ -751,7 +751,11 @@ oo applications sync --path ./applications/2026 --verbose oo applications sync --path ./applications/2026 --apply --verbose ``` -Each `README.md` is parsed for company, role, email, phone, LinkedIn, etc. +Each dossier `README.md` is parsed for company, role, email, phone, LinkedIn, etc. +Discovery walks `--path` and **skips** junk trees (`node_modules`, `tools`, `.venv`, +`pdfs`, …). Only folders that look like CV application slugs (`source-NNN-…`, or +long hyphenated dossiers) are synced — not npm package READMEs. + The sync creates or finds contacts, opens a deal, adds members, and appends a history note. Re-running is safe: duplicate members and duplicate deal titles are skipped when already present. diff --git a/cmd/oo/applications/applications.go b/cmd/oo/applications/applications.go index f4ceba7..3ed05b7 100644 --- a/cmd/oo/applications/applications.go +++ b/cmd/oo/applications/applications.go @@ -24,6 +24,33 @@ import ( var docExt = map[string]bool{".pdf": true, ".docx": true, ".xlsx": true, ".doc": true, ".xls": true} +// Directories that are never application dossiers (WalkDir SkipDir). +var skipDirNames = map[string]bool{ + "pdfs": true, "node_modules": true, ".git": true, ".venv": true, "venv": true, + "vendor": true, "tools": true, "dist": true, "build": true, ".pnpm": true, + "coverage": true, "__pycache__": true, "out": true, ".turbo": true, ".next": true, +} + +var appFolderNum = regexp.MustCompile(`^\d{2,4}$`) + +// looksLikeAppFolder accepts CV dossier slugs (uae-009-…, djinni-001-…, linkedin-elastic-…). +func looksLikeAppFolder(name string) bool { + if name == "" || strings.HasPrefix(name, ".") { + return false + } + parts := strings.Split(name, "-") + if len(parts) < 3 { + return false + } + for _, p := range parts { + if appFolderNum.MatchString(p) { + return true + } + } + // Rare dossiers without NNN (e.g. alex-staff-cloud-platform-engineer) + return len(parts) >= 4 && len(name) >= 24 +} + type RecruiterInfo struct { First, Last, JobTitle, Company, Email, Phone, LinkedIn string } @@ -51,7 +78,7 @@ func Discover(base string) ([]string, error) { return err } if d.IsDir() { - if d.Name() == "pdfs" { + if skipDirNames[d.Name()] { return fs.SkipDir } return nil @@ -63,6 +90,9 @@ func Discover(base string) ([]string, error) { if filepath.Clean(dir) == filepath.Clean(base) { return nil } + if !looksLikeAppFolder(filepath.Base(dir)) { + return nil + } out = append(out, path) return nil }) @@ -97,9 +127,13 @@ func ParseReadme(path string) (Data, error) { app.Source = filepath.Base(filepath.Dir(filepath.Dir(path))) } if app.Company == "" || strings.EqualFold(app.Company, "undisclosed") || app.Company == "?" || strings.EqualFold(app.Company, "tbd") { + // Prefer slug token after NNN (uae-009-dizzaract-… → Dizzaract), not source prefix. parts := strings.Split(filepath.Base(filepath.Dir(path)), "-") - if len(parts) >= 2 { - app.Company = titleWord(parts[0]) + for i, p := range parts { + if appFolderNum.MatchString(p) && i+1 < len(parts) { + app.Company = titleWord(parts[i+1]) + break + } } } app.Summary = buildSummary(app, text) diff --git a/cmd/oo/applications/applications_test.go b/cmd/oo/applications/applications_test.go index 62b4f35..a0ec2fe 100644 --- a/cmd/oo/applications/applications_test.go +++ b/cmd/oo/applications/applications_test.go @@ -1,6 +1,8 @@ package applications import ( + "os" + "path/filepath" "testing" onlyoffice "github.com/eslider/go-onlyoffice" @@ -15,6 +17,47 @@ func TestDealTitleForApplication(t *testing.T) { } } +func TestLooksLikeAppFolder(t *testing.T) { + cases := map[string]bool{ + "uae-009-dizzaract-senior-backend-devops-abu-dhabi": true, + "djinni-001-devops-engineer-windows-cloud-justmarkets": true, + "linkedin-elastic-senior-go-intake-observability": true, // no NNN but long + "alex-staff-cloud-platform-engineer": true, + "http-client": false, + "node_modules": false, + "tools": false, + "README": false, + "uae-009": false, // too short + } + for name, want := range cases { + if got := looksLikeAppFolder(name); got != want { + t.Errorf("looksLikeAppFolder(%q)=%v want %v", name, got, want) + } + } +} + +func TestDiscoverSkipsNodeModules(t *testing.T) { + root := t.TempDir() + appDir := filepath.Join(root, "uae-009-dizzaract-senior-backend-devops-abu-dhabi") + junk := filepath.Join(root, "node_modules", "http-client") + tools := filepath.Join(root, "tools", "gmail-oo-reconcile") + for _, d := range []string{appDir, junk, tools} { + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(d, "README.md"), []byte("# x\n"), 0o644); err != nil { + t.Fatal(err) + } + } + got, err := Discover(root) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || filepath.Base(filepath.Dir(got[0])) != filepath.Base(appDir) { + t.Fatalf("Discover=%v want only %s", got, appDir) + } +} + func TestHasContactInfoHelper(t *testing.T) { contact := map[string]any{ "commonData": []any{