Files
scripts/okf-hub/okf-validate.sh
T

353 lines
12 KiB
Bash
Executable File

#!/bin/bash
# okf-validate.sh — Validação OKF pre-commit para o Hub Obsidian
#
# Instalar: bash scripts/install-hooks.sh
# Executar manualmente: bash scripts/okf-validate.sh [--all] [--warn-only]
#
# Comportamento:
# Sem args → valida apenas ficheiros staged (para pre-commit)
# --all → valida todos os ficheiros activos do vault
# --warn-only → não bloqueia o commit (só avisos)
#
# Criado: 28-06-2026
set -euo pipefail
VAULT="/media/ealmeida/Dados/Hub"
ERRORS=0
WARNINGS=0
WARN_ONLY=false
ALL_FILES=false
# Parsing de argumentos
for arg in "$@"; do
case "$arg" in
--warn-only) WARN_ONLY=true ;;
--all) ALL_FILES=true ;;
esac
done
# Cores para output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
err() { echo -e "${RED}[ERRO]${NC} $*" >&2; ERRORS=$((ERRORS+1)); }
warn() { echo -e "${YELLOW}[AVISO]${NC} $*" >&2; WARNINGS=$((WARNINGS+1)); }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
# ─── Determinar ficheiros a validar ───────────────────────────────────────────
get_files() {
if [[ "$ALL_FILES" == "true" ]]; then
# Todos os .md activos (excluir arquivo, stversions, gitignore)
find "$VAULT" -name "*.md" \
-not -path "*/99-Arquivo/*" \
-not -path "*/.stversions/*" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/.obsidian/*" \
-not -path "*/.ijfw/*" \
-not -path "*/\.trash/*" \
2>/dev/null
else
# Só ficheiros staged (modo pre-commit)
git -C "$VAULT" diff --cached --name-only --diff-filter=ACM 2>/dev/null \
| grep "\.md$" \
| while IFS= read -r f; do echo "$VAULT/$f"; done
fi
}
# ─── Regras de exclusão ───────────────────────────────────────────────────────
should_skip() {
local file="$1"
local basename
basename=$(basename "$file")
local filepath_lower
filepath_lower=$(echo "$file" | tr '[:upper:]' '[:lower:]')
# OKF reserved — sem frontmatter obrigatório
[[ "$basename" == "index.md" ]] && return 0
[[ "$basename" == "log.md" ]] && return 0
# Ficheiros de sistema/config — não são documentos OKF
[[ "$basename" == "CLAUDE.md" ]] && return 0
[[ "$basename" == "AGENTS.md" ]] && return 0
[[ "$basename" == "README.md" ]] && return 0
[[ "$basename" == "README.txt" ]] && return 0
[[ "$basename" == ".desk-project" ]] && return 0
# Paths internos de ferramentas e config
[[ "$filepath_lower" == *"/.wayland/"* ]] && return 0
[[ "$filepath_lower" == *"/.hermes/"* ]] && return 0
[[ "$filepath_lower" == *"/ijfw/"* ]] && return 0
[[ "$filepath_lower" == *"/.github/"* ]] && return 0
[[ "$filepath_lower" == *"/\.obsidian/"* ]] && return 0
# Relatórios de scripts (gerados automaticamente)
[[ "$basename" == "okf-normalize-report.md" ]] && return 0
[[ "$basename" == "project-journal.md" ]] && return 0
return 1
}
# ─── Verificações ─────────────────────────────────────────────────────────────
check_type_field() {
local file="$1"
if ! grep -q "^type:" "$file" 2>/dev/null; then
err "Sem 'type:' — $file"
return
fi
# Verificar valor válido
local type_val
type_val=$(grep "^type:" "$file" | head -1 | sed 's/^type: *//' | tr -d '"'"'" | xargs)
# Tipos OKF canónicos
local canonical_types=(
"Document" "Index" "Playbook" "Reference" "Specification"
"Status" "Template" "Changelog" "Proposal" "Client Profile"
"Concept" "Decision" "Guide" "Record" "Runbook"
)
# Tipos legacy (pré-OKF) — aceites sem aviso para não bloquear trabalho normal
local legacy_types=(
"note" "Note" "concept" "manual" "Manual" "procedimento"
"procedure" "reference" "spec" "taskforce" "api" "departamento"
"report" "plan" "deep-research" "research-report" "research"
"documentation" "componente-ia" "proposta" "pesquisa"
"deep-research-prompt" "audit" "relatorio-ic" "runbook"
"guide" "record" "decision" "Worklist" "meeting-notes"
"journal" "review" "analysis" "summary" "overview"
# Hub-specific legacy types descobertos via okf info/validate
"schema" "proc" "servico" "redirect" "diagnostic"
"deep-research-sources" "source-list" "documentacao"
"agente" "analise" "archive-marker" "arquitectura" "arquivo"
"checklist" "checkpoint" "checkup" "checkup-consolidado"
"componente-infra" "content" "continuacao" "conversa-exportada"
"daily" "decisao"
"auditoria" "design-spec" "diagnostico" "diario" "doc"
"documentacao-tecnica" "estrategia" "evaluation" "evidencia"
"final-report" "fontes-curadas" "framework" "inbox" "insight"
"mapeamento" "metricas" "notes" "planning" "plano"
"plano-execucao" "procedimento-infra" "process-map" "product-spec"
"projecto" "prompt" "prompts-pesquisa" "reconnaissance"
"registo-historico" "relatorio-pesquisa" "reuniao" "revisao"
"roadmap" "session-handoff" "sintese" "sistema" "spec-design"
"spec-umbrella" "status" "tarefa" "triangulacao" "troubleshooting"
"visao" "worklog"
)
local found=false
for vt in "${canonical_types[@]}" "${legacy_types[@]}"; do
[[ "$type_val" == "$vt" ]] && found=true && break
done
if [[ "$found" == "false" ]]; then
warn "type desconhecido '$type_val' — usar: Document, Playbook, Reference, Specification, Status — $file"
fi
}
check_description_field() {
local file="$1"
if ! grep -q "^description:" "$file" 2>/dev/null; then
# Só aviso — não bloqueia
warn "Sem 'description:' — $file"
fi
}
check_timestamp_field() {
local file="$1"
# Aceitar 'timestamp:' OU 'date:' (muitos ficheiros antigos têm 'date:')
if ! grep -qE "^(timestamp|date):" "$file" 2>/dev/null; then
warn "Sem 'timestamp:' — $file"
fi
}
check_uppercase_index() {
local file="$1"
local basename
basename=$(basename "$file")
# Bloquear criação de INDEX.md maiúsculo (deprecated desde 28-06-2026)
if [[ "$basename" == "INDEX.md" ]]; then
err "INDEX.md uppercase está deprecated desde 28-06-2026 — usar 'index.md' — $file"
fi
}
check_index_wikilinks() {
local file="$1"
local basename
basename=$(basename "$file")
if [[ "$basename" == "index.md" ]]; then
if grep -q "\[\[" "$file" 2>/dev/null; then
local count
count=$(grep -c "\[\[" "$file" 2>/dev/null || echo 0)
warn "index.md com $count wikilinks — converter para [texto](path.md) — $file"
fi
fi
}
check_qr_line_limit() {
local file="$1"
local basename
basename=$(basename "$file")
if [[ "$basename" == QR-*.md ]]; then
local lines
lines=$(wc -l < "$file" 2>/dev/null || echo 0)
if [[ "$lines" -gt 350 ]]; then
# Erro só para QR verdadeiramente gigantes (>350) — indica decomposição urgente
err "QR-*.md excede 350 linhas ($lines) — dividir imediatamente — $file"
elif [[ "$lines" -gt 200 ]]; then
# Aviso para QR entre 200-350 — dívida técnica, não bloqueia
warn "QR-*.md excede 200 linhas ($lines) — dividir quando possível — $file"
fi
fi
}
check_index_size() {
local file="$1"
local basename
basename=$(basename "$file")
if [[ "$basename" == "index.md" ]]; then
local lines
lines=$(wc -l < "$file" 2>/dev/null || echo 0)
if [[ "$lines" -gt 100 ]]; then
warn "index.md muito longo ($lines linhas, max recomendado: 80) — $file"
fi
fi
}
check_no_content_in_index() {
local file="$1"
local basename
basename=$(basename "$file")
if [[ "$basename" != "index.md" ]]; then return; fi
# Contar linhas de conteúdo substantivo (não links, não headings, não vazias, não frontmatter)
local subst_lines
subst_lines=$(awk '
/^---$/ { in_fm = !in_fm; next }
in_fm { next }
/^\s*$/ { next }
/^#/ { next }
/^\[/ { next }
/^\|/ { next }
/^>/ { next }
/^```/ { next }
{ count++ }
END { print count+0 }
' "$file" 2>/dev/null || echo 0)
if [[ "$subst_lines" -gt 5 ]]; then
warn "index.md tem $subst_lines linhas de conteúdo substantivo — index.md deve conter só links — $file"
fi
}
check_sync_conflicts() {
local file="$1"
if [[ "$file" == *".sync-conflict-"* ]]; then
warn "Ficheiro sync-conflict a ser commitado — resolver antes — $file"
fi
}
# ─── Verificação de links quebrados via okf CLI ───────────────────────────────
check_broken_links_okf() {
if ! command -v okf &>/dev/null; then
return 0
fi
if [[ "$ALL_FILES" != "true" ]]; then
return 0 # só correr em modo --all (vault completo)
fi
info "OKF CLI: a verificar links quebrados no bundle..."
local okf_out
okf_out=$(okf validate "$VAULT" 2>&1) || true
# Filtrar: excluir erros de directórios ocultos (dot-paths: .ijfw, .stversions, .github, .wayland)
# que o okf não sabe ignorar — são erros de parsing, não broken links
local broken_lines
broken_lines=$(echo "$okf_out" \
| grep -iE "broken|not found|missing link" \
| grep -v "Invalid concept id segment" \
| grep -v "/\." \
2>/dev/null || true)
if [[ -n "$broken_lines" ]]; then
local count
count=$(echo "$broken_lines" | wc -l | tr -d ' ')
warn "OKF CLI: $count links quebrados detectados"
echo "$broken_lines" | head -30 >&2
fi
}
# ─── Loop principal ────────────────────────────────────────────────────────────
echo ""
info "=== OKF Validation $(date '+%Y-%m-%d %H:%M') ==="
if [[ "$ALL_FILES" == "true" ]]; then
info "Modo: COMPLETO (todos os ficheiros activos)"
else
info "Modo: STAGED (ficheiros em staging)"
fi
echo ""
FILE_COUNT=0
mapfile -t files < <(get_files)
for file in "${files[@]}"; do
[[ -z "$file" ]] && continue
[[ ! -f "$file" ]] && continue
if should_skip "$file"; then
continue
fi
FILE_COUNT=$((FILE_COUNT+1))
check_uppercase_index "$file"
check_sync_conflicts "$file"
check_type_field "$file"
check_description_field "$file"
check_timestamp_field "$file"
check_index_wikilinks "$file"
check_index_size "$file"
check_no_content_in_index "$file"
check_qr_line_limit "$file"
done
# Verificação de links quebrados (só em modo --all)
check_broken_links_okf
# ─── Sumário ──────────────────────────────────────────────────────────────────
echo ""
echo "─────────────────────────────────────────"
info "Ficheiros validados: $FILE_COUNT"
if [[ $WARNINGS -gt 0 ]]; then
echo -e "${YELLOW}Avisos: $WARNINGS${NC}"
fi
if [[ $ERRORS -gt 0 ]]; then
echo -e "${RED}Erros: $ERRORS${NC}"
echo ""
if [[ "$WARN_ONLY" == "true" ]]; then
warn "Modo --warn-only: commit não bloqueado apesar de $ERRORS erros"
exit 0
else
err "Commit bloqueado — corrigir erros OKF antes de commitar"
echo " Dica: bash scripts/okf-validate.sh --warn-only (para forçar)"
echo " Dica: bash scripts/okf-normalize.py (para auto-corrigir frontmatter)"
exit 1
fi
else
ok "OKF Validation PASSED ($FILE_COUNT ficheiros, $WARNINGS avisos)"
fi
# ─── OKF Bundle Inventory (modo --all) ───────────────────────────────────────
if command -v okf &>/dev/null && [[ "$ALL_FILES" == "true" ]]; then
echo ""
info "=== OKF Bundle Inventory ==="
okf info "$VAULT" 2>/dev/null || true
fi
exit ${ERRORS:-0}