From 8d4988ad3fc52ed161cb3031d4645b88c05eff02 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Sun, 28 Jun 2026 21:14:39 +0100 Subject: [PATCH] fix(okf-hub): adoptar validador layer-aware (Fase D) + corrigir bugs (reserved-skip, folded-scalar desc, tr range); classify exclui index.md/log.md --- okf-hub/okf-classify-layer.sh | 210 +++++++++++++ okf-hub/okf-validate.sh | 560 +++++++++++++++------------------- 2 files changed, 449 insertions(+), 321 deletions(-) create mode 100755 okf-hub/okf-classify-layer.sh diff --git a/okf-hub/okf-classify-layer.sh b/okf-hub/okf-classify-layer.sh new file mode 100755 index 0000000..d3e16ce --- /dev/null +++ b/okf-hub/okf-classify-layer.sh @@ -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)" diff --git a/okf-hub/okf-validate.sh b/okf-hub/okf-validate.sh index b9582eb..81ace68 100755 --- a/okf-hub/okf-validate.sh +++ b/okf-hub/okf-validate.sh @@ -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}