From 692d243157ffe29bed29a4be60cf9b7e1e498640 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 3 May 2026 19:34:07 -0300 Subject: [PATCH] fix: unignore shared/workspace package; add missing file to git --- .gitignore | 1 + shared/workspace/workspace.go | 200 ++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 shared/workspace/workspace.go diff --git a/.gitignore b/.gitignore index edd2358..beab247 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin/ # Workspace — conteúdo pessoal dos posts workspace/ workspace-test/ +!shared/workspace/ # Go build cache *.test diff --git a/shared/workspace/workspace.go b/shared/workspace/workspace.go new file mode 100644 index 0000000..0c139ac --- /dev/null +++ b/shared/workspace/workspace.go @@ -0,0 +1,200 @@ +package workspace + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "unicode" +) + +// ─── Path helpers ───────────────────────────────────────────────────────────── + +// PostPath returns the post root directory: // +func PostPath(workspaceRoot, categoria, slug string) string { + return filepath.Join(workspaceRoot, categoria, slug) +} + +// InputPath returns the input/ subdirectory of a post. +func InputPath(postPath string) string { + return filepath.Join(postPath, "input") +} + +// WorkPath returns the work/ subdirectory of a post. +func WorkPath(postPath string) string { + return filepath.Join(postPath, "work") +} + +// OutputPath returns the output/ subdirectory of a post. +func OutputPath(postPath string) string { + return filepath.Join(postPath, "output") +} + +// ArtRawPath returns the work/art-raw/ subdirectory. +func ArtRawPath(postPath string) string { + return filepath.Join(postPath, "work", "art-raw") +} + +// InputTextoPath returns the path to input/texto.md. +func InputTextoPath(postPath string) string { + return filepath.Join(InputPath(postPath), "texto.md") +} + +// OutputPostPath returns the path to output/post.md. +func OutputPostPath(postPath string) string { + return filepath.Join(OutputPath(postPath), "post.md") +} + +// ArtPromptsPath returns work/art-prompts.json. +func ArtPromptsPath(postPath string) string { + return filepath.Join(WorkPath(postPath), "art-prompts.json") +} + +// InboxSugestoes returns the inbox sugestões file path. +func InboxSugestoes(workspaceRoot string) string { + return filepath.Join(workspaceRoot, "_inbox", "_sugestoes.md") +} + +// VersionedFile returns the path for a versioned file like redator-v1.md. +func VersionedFile(postPath, prefix string, n int) string { + return filepath.Join(WorkPath(postPath), fmt.Sprintf("%s-v%d.md", prefix, n)) +} + +// ─── Discovery ──────────────────────────────────────────────────────────────── + +// FindVariantsByBase returns all postPaths whose slug is exactly baseSlug OR +// starts with baseSlug+"--" (recycled variants like "rag-csharp--porque"). +// Only paths that have a work/ subdirectory are included. +func FindVariantsByBase(workspaceRoot, baseSlug string) []string { + var found []string + cats, err := os.ReadDir(workspaceRoot) + if err != nil { + return nil + } + prefix := baseSlug + "--" + for _, cat := range cats { + if !cat.IsDir() || strings.HasPrefix(cat.Name(), "_") || strings.HasPrefix(cat.Name(), ".") { + continue + } + catPath := filepath.Join(workspaceRoot, cat.Name()) + slugs, err := os.ReadDir(catPath) + if err != nil { + continue + } + for _, s := range slugs { + if !s.IsDir() { + continue + } + name := s.Name() + if name == baseSlug || strings.HasPrefix(name, prefix) { + candidate := filepath.Join(catPath, name) + if _, err := os.Stat(WorkPath(candidate)); err == nil { + found = append(found, candidate) + } + } + } + } + return found +} + +// FindPostBySlug searches all category subdirectories for a post with the given +// slug. Returns the postPath on success. +func FindPostBySlug(workspaceRoot, slug string) (string, error) { + entries, err := os.ReadDir(workspaceRoot) + if err != nil { + return "", fmt.Errorf("ler workspace %s: %w", workspaceRoot, err) + } + for _, e := range entries { + if !e.IsDir() || strings.HasPrefix(e.Name(), "_") || strings.HasPrefix(e.Name(), ".") { + continue + } + candidate := PostPath(workspaceRoot, e.Name(), slug) + if _, err := os.Stat(WorkPath(candidate)); err == nil { + return candidate, nil + } + } + return "", fmt.Errorf("slug %q não encontrado em %s", slug, workspaceRoot) +} + +// LatestVersionFile scans work/ for files matching -vN.md and returns +// the path and version number of the highest version, or ("", 0) if none. +func LatestVersionFile(postPath, prefix string) (string, int) { + maxN := 0 + for n := 1; n <= 99; n++ { + path := VersionedFile(postPath, prefix, n) + if _, err := os.Stat(path); err == nil { + maxN = n + } else { + break + } + } + if maxN == 0 { + return "", 0 + } + return VersionedFile(postPath, prefix, maxN), maxN +} + +// ─── Directory setup ────────────────────────────────────────────────────────── + +// EnsureDirs creates input/, work/, output/, and work/art-raw/ for a post. +func EnsureDirs(postPath string) error { + dirs := []string{ + InputPath(postPath), + WorkPath(postPath), + OutputPath(postPath), + ArtRawPath(postPath), + } + for _, d := range dirs { + if err := os.MkdirAll(d, 0755); err != nil { + return fmt.Errorf("criar %s: %w", d, err) + } + } + return nil +} + +// ─── Slug ───────────────────────────────────────────────────────────────────── + +// SlugFromTitle converts a human-readable title to a lowercase ASCII slug. +// "Como criar RAG com C#: guia completo" → "como-criar-rag-com-csharp-guia-completo" +func SlugFromTitle(title string) string { + // Replace common non-ASCII sequences before lowercasing + replacements := map[string]string{ + "C#": "csharp", + "C++": "cpp", + ".NET": "dotnet", + "@": "at", + } + for old, newVal := range replacements { + title = strings.ReplaceAll(title, old, newVal) + } + + title = strings.ToLower(title) + + var result []rune + for _, r := range title { + switch { + case unicode.IsLetter(r) || unicode.IsDigit(r): + result = append(result, r) + case r == ' ' || r == '-' || r == '_' || r == ':' || r == '/': + if len(result) > 0 && result[len(result)-1] != '-' { + result = append(result, '-') + } + } + // All other chars (accents, punctuation) dropped + } + + slug := strings.Trim(string(result), "-") + + // Normalize accented chars with simple replacements + slug = strings.NewReplacer( + "ã", "a", "á", "a", "â", "a", "à", "a", + "é", "e", "ê", "e", "è", "e", + "í", "i", "î", "i", + "ó", "o", "ô", "o", "õ", "o", + "ú", "u", "û", "u", + "ç", "c", + "ñ", "n", + ).Replace(slug) + + return slug +}