#!/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}