fix: unignore shared/workspace package; add missing file to git

This commit is contained in:
Ricardo Carneiro 2026-05-03 19:34:07 -03:00
parent ea532659b0
commit 692d243157
2 changed files with 201 additions and 0 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ bin/
# Workspace — conteúdo pessoal dos posts
workspace/
workspace-test/
!shared/workspace/
# Go build cache
*.test

View File

@ -0,0 +1,200 @@
package workspace
import (
"fmt"
"os"
"path/filepath"
"strings"
"unicode"
)
// ─── Path helpers ─────────────────────────────────────────────────────────────
// PostPath returns the post root directory: <workspace>/<categoria>/<slug>
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 <prefix>-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
}