fix(okf-hub): adoptar validador layer-aware (Fase D) + corrigir bugs (reserved-skip, folded-scalar desc, tr range); classify exclui index.md/log.md

This commit is contained in:
2026-06-28 21:14:39 +01:00
parent 6035542b67
commit 8d4988ad3f
2 changed files with 449 additions and 321 deletions
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# okf-classify-layer.sh — atribui layer: raw|wiki a todos os .md do Hub
# Heurísticas de path + conteúdo (§6 Fase C do OKF-Compliance-Plan-Global.md)
# Uso: okf-classify-layer.sh [--dry-run]
# --dry-run mostra o que faria sem alterar ficheiros
set -euo pipefail
HUB_ROOT="/media/ealmeida/Dados/Hub"
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
# --- Exclusion patterns ---
is_excluded() {
local rel="$1"
[[ "$rel" == *99-Arquivo/* ]] && return 0
[[ "$rel" == *.stversions/* ]] && return 0
[[ "$rel" == *.obsidian/* ]] && return 0
[[ "$rel" == *.ijfw/* ]] && return 0
[[ "$rel" == *_templates/* ]] && return 0
[[ "$rel" == *90-Templates/* ]] && return 0
[[ "$rel" == *.git/* ]] && return 0
[[ "$rel" == *node_modules/* ]] && return 0
[[ "$rel" == *.github/* ]] && return 0
[[ "$rel" == *.wayland/* ]] && return 0
[[ "$rel" == *.hermes/* ]] && return 0
local base
base="$(basename "$rel")"
[[ "$base" == "MEMORY.md" ]] && return 0
[[ "$base" == "index.md" ]] && return 0
[[ "$base" == "log.md" ]] && return 0
[[ "$base" == "CLAUDE.md" ]] && return 0
[[ "$base" == "GEMINI.md" ]] && return 0
[[ "$base" == "AGENTS.md" ]] && return 0
[[ "$base" == "copilot-instructions.md" ]] && return 0
return 1
}
# --- Classification rules (ordered by priority, first match wins) ---
classify_file() {
local f="$1"
local rel="${f#$HUB_ROOT/}"
local base
base="$(basename "$rel")"
local desc_len=0
# --- RAW rules (signals: machine-generated, regenerable, dumps) ---
# 1. YouTube transcript dumps
[[ "$rel" == *KB/MDs/YouTube/* ]] && echo "raw" && return
# 2. Deep research in bulk
[[ "$rel" == *deep-research* ]] && echo "raw" && return
# 3. Raw research data (FinalKintsugi/research/)
[[ "$rel" == *FinalKintsugi/research/* ]] && echo "raw" && return
# 4. YouTube transcript dumps (Knowledge-Base/MDs/YouTube/)
[[ "$rel" == *Knowledge-Base/MDs/YouTube/* ]] && echo "raw" && return
# 5. Inteligência Competitiva daily reports (dated pattern)
if [[ "$rel" == *Inteligencia/* ]]; then
# Dated IC reports
if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}[-_]'; then
echo "raw" && return
fi
[[ "$base" == *"deep-research"* ]] && echo "raw" && return
[[ "$base" == *"Fontes"*"Canal"* ]] && echo "raw" && return
fi
# 5. SistemaBackups dated reports
if [[ "$rel" == *SistemaBackups/* ]]; then
if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}[-_]'; then
echo "raw" && return
fi
# Audit/relatório reports
if echo "$base" | grep -qiE '^(AUDITORIA|RELATORIO|TRIANGULACAO|SINCRONIZACAO)-'; then
echo "raw" && return
fi
fi
# 6. Observabilidade reports
if [[ "$rel" == *Observabilidade/* ]]; then
if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}[-_]'; then
echo "raw" && return
fi
fi
# 7. SelfImprovement dated docs
if [[ "$rel" == *SelfImprovement/* ]]; then
if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}[-_]'; then
echo "raw" && return
fi
fi
# 8. Clip dated reports
if [[ "$rel" == *Clip/* ]]; then
if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}[-_]'; then
echo "raw" && return
fi
# Clip incident reports
[[ "$base" == *"incidentes"* ]] && echo "raw" && return
[[ "$base" == *"wordpress-update-report"* ]] && echo "raw" && return
fi
# 9. Worklogs, daily reports, ponto-situação
if echo "$base" | grep -qiE '(worklog|ponto-situacao|checkup|diario|daily)'; then
echo "raw" && return
fi
# 10. OpenDesign scratch
if [[ "$rel" == *OpenDesign/* ]] || [[ "$rel" == *open-design/* ]]; then
if echo "$base" | grep -qiE 'analise-capacidades'; then
echo "raw" && return
fi
fi
# 11. Knowledge-Base large reference files (from old import)
if [[ "$rel" == *Knowledge-Base/* ]]; then
local fsize
fsize=$(wc -c < "$f" 2>/dev/null || echo 0)
if [[ "$fsize" -gt 50000 ]]; then
echo "raw" && return
fi
fi
# --- WIKI rules (everything else) ---
# Standard wiki patterns
case "$base" in
PROC-*|QR-*|SPEC-*|*-SPEC.md|STATUS.md|CHANGELOG.md|index.md)
echo "wiki" && return ;;
esac
# Has PROC/QR/SPEC in path
[[ "$rel" == *Procedimentos/* ]] && echo "wiki" && return
[[ "$rel" == *Quick-Reference/* ]] && echo "wiki" && return
[[ "$rel" == *specs/* ]] && echo "wiki" && return
[[ "$rel" == *plans/* ]] && echo "wiki" && return
# Default to wiki
echo "wiki"
}
# --- Inject layer into frontmatter ---
inject_layer() {
local f="$1"
local layer="$2"
local rel="${f#$HUB_ROOT/}"
# Check if already has layer
if head -20 "$f" | grep -q '^layer:'; then
return
fi
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY-RUN: $rel → layer: $layer"
return
fi
# Insert layer after first line (---) if frontmatter exists
if head -1 "$f" | grep -q '^---$'; then
# Insert after line 1
sed -i "1a\\layer: $layer" "$f"
else
# No frontmatter — add layer block
local tmp
tmp=$(mktemp)
{
echo "---"
echo "layer: $layer"
echo "---"
cat "$f"
} > "$tmp"
mv "$tmp" "$f"
fi
}
# --- Main ---
RAW=0
WIKI=0
SKIPPED=0
TOTAL=0
echo "Classifying files in $HUB_ROOT..."
[[ "$DRY_RUN" == "true" ]] && echo "(DRY-RUN mode — no files modified)"
echo ""
while IFS= read -r -d '' f; do
rel="${f#$HUB_ROOT/}"
if is_excluded "$rel"; then
((SKIPPED++)) || true
continue
fi
((TOTAL++)) || true
layer="$(classify_file "$f")"
inject_layer "$f" "$layer"
case "$layer" in
raw) ((RAW++)) || true ;;
wiki) ((WIKI++)) || true ;;
esac
done < <(find "$HUB_ROOT" -name '*.md' -not -path '*/.git/*' -not -path '*/node_modules/*' -print0)
echo ""
echo "────────────────────────────────────"
echo "Total: $TOTAL | Raw: $RAW | Wiki: $WIKI | Skipped: $SKIPPED"
echo ""
[[ "$DRY_RUN" == "true" ]] && echo "(Re-run without --dry-run to apply)"
+239 -321
View File
@@ -1,352 +1,270 @@
#!/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
#!/usr/bin/env bash
# okf-validate.sh — validador OKF v0.1 para o Hub Obsidian
# Uso: okf-validate.sh [--all] [--strict] [file ...]
# --all valida todos os .md do Hub (exclui exclusões)
# --strict aplica regras layer:wiki (description≥45ch, tags≥2)
# Sem args + stdin: valida ficheiros staged (pre-commit hook)
set -euo pipefail
VAULT="/media/ealmeida/Dados/Hub"
HUB_ROOT="/media/ealmeida/Dados/Hub"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# --- Exclusion patterns (relative to HUB_ROOT) ---
EXCLUDE_PATTERNS=(
"99-Arquivo/"
".stversions/"
".obsidian/"
".ijfw/"
"_templates/"
"90-Templates/"
".git/"
"node_modules/"
".github/"
".wayland/"
".hermes/"
)
# Exclude specific filenames
EXCLUDE_FILES=(
"MEMORY.md"
"CLAUDE.md"
"GEMINI.md"
"AGENTS.md"
"copilot-instructions.md"
)
# Canonical type vocabulary (§5 of OKF-Compliance-Plan-Global.md)
CANONICAL_TYPES=(
"Playbook"
"Reference"
"Specification"
"Status"
"Changelog"
"Index"
"Template"
"Client Profile"
"Document"
"note"
)
ERRORS=0
WARNINGS=0
WARN_ONLY=false
ALL_FILES=false
CHECKED=0
STRICT=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
YEL='\033[0;33m'
GRN='\033[0;32m'
NC='\033[0m'
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} $*"; }
is_excluded() {
local f="$1"
local rel="${f#$HUB_ROOT/}"
# ─── Determinar ficheiros a validar ───────────────────────────────────────────
# Check directory patterns
for pat in "${EXCLUDE_PATTERNS[@]}"; do
if [[ "$rel" == *"$pat"* ]]; then
return 0
fi
done
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
# Check filename patterns
local base
base="$(basename "$rel")"
for pat in "${EXCLUDE_FILES[@]}"; do
if [[ "$base" == "$pat" ]]; then
return 0
fi
done
return 1
}
# ─── Regras de exclusão ───────────────────────────────────────────────────────
is_index_subfolder() {
# index.md in subfolders don't need frontmatter (anti-pattern)
local f="$1"
local base
base="$(basename "$f")"
local dir
dir="$(dirname "$f")"
local parent_base
parent_base="$(basename "$dir")"
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"
# Root index.md DOES need frontmatter
if [[ "$dir" == "$HUB_ROOT" ]]; then
return 1
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"
if [[ "$base" == "index.md" ]]; then
return 0
fi
fi
return 1
}
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"
is_log_file() {
local base
base="$(basename "$1")"
[[ "$base" == "log.md" ]]
}
validate_file() {
local f="$1"
local rel="${f#$HUB_ROOT/}"
local content
if [[ ! -f "$f" ]]; then
echo -e "${RED}ERROR${NC} $rel: file not found"
((ERRORS++)) || true
return
fi
((CHECKED++)) || true
# Reservados OKF (index.md em qualquer nível, log.md) — isentos de todas as verificações
local base; base="$(basename "$f")"
if [[ "$base" == "index.md" || "$base" == "log.md" ]]; then
return
fi
# Check frontmatter
if ! head -1 "$f" | grep -q '^---$'; then
# log.md and subfolder index.md are exempt
if is_log_file "$f" || is_index_subfolder "$f"; then
return
fi
echo -e "${RED}ERROR${NC} $rel: missing frontmatter"
((ERRORS++)) || true
return
fi
# Extract frontmatter using awk (robust: handles leading whitespace, multiline values)
content="$(awk 'BEGIN{n=0} /^---[[:space:]]*$/{n++; if(n==2) exit} n==1 && n>0{print}' "$f")"
if [[ -z "$content" ]]; then
echo -e "${RED}ERROR${NC} $rel: empty or invalid frontmatter"
((ERRORS++)) || true
return
fi
# Parse YAML fields (simple grep — no Python dependency)
local has_type has_title has_timestamp has_desc has_tags has_layer
has_type=$(echo "$content" | grep -c '^type:' || true)
has_title=$(echo "$content" | grep -c '^title:' || true)
has_timestamp=$(echo "$content" | grep -c '^timestamp:' || true)
has_desc=$(echo "$content" | grep -c '^description:' || true)
has_tags=$(echo "$content" | grep -c '^tags:' || true)
has_layer=$(echo "$content" | grep -c '^layer:' || true)
# Required for all
if [[ "$has_type" -eq 0 ]]; then
echo -e "${RED}ERROR${NC} $rel: missing 'type'"
((ERRORS++)) || true
fi
if [[ "$has_title" -eq 0 ]]; then
echo -e "${RED}ERROR${NC} $rel: missing 'title'"
((ERRORS++)) || true
fi
if [[ "$has_timestamp" -eq 0 ]]; then
echo -e "${RED}ERROR${NC} $rel: missing 'timestamp'"
((ERRORS++)) || true
fi
# Strict checks (layer:wiki or --strict flag)
if [[ "$STRICT" == "true" ]] || [[ "$has_layer" -gt 0 ]] && echo "$content" | grep -q '^layer: wiki'; then
if [[ "$has_desc" -eq 0 ]]; then
echo -e "${RED}ERROR${NC} $rel [wiki]: missing 'description'"
((ERRORS++)) || true
else
# Check description length (≥45 chars, excluding YAML key prefix)
# Comprimento da description (>=45 ch; trata folded scalars >- / |)
local desc_len
desc_len=$(echo "$content" | awk '
/^description:/{f=1; s=$0; sub(/^description:[[:space:]]*[>|+-]*[[:space:]]*/,"",s); buf=s; next}
f && /^[[:space:]]+[^[:space:]]/{t=$0; sub(/^[[:space:]]+/,"",t); buf=buf " " t; next}
f && /^[^[:space:]]/{f=0}
END{gsub(/[>|]/,"",buf); print length(buf)}
')
if [[ "$desc_len" -lt 45 ]]; then
echo -e "${RED}ERROR${NC} $rel [wiki]: description too short ($desc_len < 45 chars)"
((ERRORS++)) || true
fi
fi
if [[ "$has_tags" -eq 0 ]]; then
echo -e "${RED}ERROR${NC} $rel [wiki]: missing 'tags'"
((ERRORS++)) || true
fi
fi
# Warnings (non-blocking)
if [[ "$has_desc" -eq 0 ]]; then
echo -e "${YEL}WARN${NC} $rel: missing 'description'"
((WARNINGS++)) || true
fi
if [[ "$has_tags" -eq 0 ]]; then
echo -e "${YEL}WARN${NC} $rel: missing 'tags'"
((WARNINGS++)) || true
fi
# Check for wikilinks in body (wiki layer)
if echo "$content" | grep -q '^layer: wiki'; then
local body
body="$(sed -n '/^---$/,/^---$/d; p' "$f")"
if echo "$body" | grep -q '\[\['; then
echo -e "${YEL}WARN${NC} $rel [wiki]: contains wikilinks [[ ]] — convert to [text](path)"
((WARNINGS++)) || true
fi
fi
fi
}
check_no_content_in_index() {
local file="$1"
local basename
basename=$(basename "$file")
if [[ "$basename" != "index.md" ]]; then return; fi
# --- Main ---
FILES=()
ALL=false
# 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"
for arg in "$@"; do
case "$arg" in
--all) ALL=true ;;
--strict) STRICT=true ;;
*) FILES+=("$arg") ;;
esac
done
# Verificação de links quebrados (só em modo --all)
check_broken_links_okf
if [[ "$ALL" == "true" ]]; then
while IFS= read -r -d '' f; do
if ! is_excluded "$f"; then
FILES+=("$f")
fi
done < <(find "$HUB_ROOT" -name '*.md' -not -path '*/.git/*' -not -path '*/node_modules/*' -print0)
elif [[ ${#FILES[@]} -eq 0 ]]; then
# Pre-commit mode: read staged files from git
while IFS= read -r f; do
if [[ "$f" == *.md ]] && ! is_excluded "$HUB_ROOT/$f"; then
FILES+=("$HUB_ROOT/$f")
fi
done < <(git -C "$HUB_ROOT" diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
fi
# ─── Sumário ──────────────────────────────────────────────────────────────────
if [[ ${#FILES[@]} -eq 0 ]]; then
echo "No .md files to validate."
exit 0
fi
echo "Validating ${#FILES[@]} files..."
echo ""
for f in "${FILES[@]}"; do
validate_file "$f"
done
echo ""
echo "─────────────────────────────────────────"
info "Ficheiros validados: $FILE_COUNT"
echo "────────────────────────────────────"
echo -e "Checked: $CHECKED | ${GRN}OK: $((CHECKED - ERRORS - WARNINGS))${NC} | ${YEL}Warnings: $WARNINGS${NC} | ${RED}Errors: $ERRORS${NC}"
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)"
if [[ "$ERRORS" -gt 0 ]]; then
echo -e "${RED}VALIDATION FAILED${NC}"
exit 1
fi
else
ok "OKF Validation PASSED ($FILE_COUNT ficheiros, $WARNINGS avisos)"
echo -e "${GRN}VALIDATION PASSED${NC}"
exit 0
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}