feat(okf-hub): relocar tooling OKF do Hub para Dev/Scripts (regra: scripts fora do vault)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Artefactos gerados (regeneráveis pelos scripts) — não versionar
|
||||
okf-normalize-report.md
|
||||
okf-rename-index.log
|
||||
hub-okf-graph.dot
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# install-hooks.sh — Instala os git hooks OKF no Hub vault
|
||||
# Uso: bash scripts/install-hooks.sh [--uninstall]
|
||||
#
|
||||
# Criado: 28-06-2026
|
||||
|
||||
VAULT="/media/ealmeida/Dados/Hub"
|
||||
SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GIT_DIR=$(git -C "$VAULT" rev-parse --git-dir 2>/dev/null)
|
||||
|
||||
if [[ -z "$GIT_DIR" ]]; then
|
||||
echo "ERRO: $VAULT não é um repositório git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolver path absoluto do .git
|
||||
if [[ "$GIT_DIR" == ".git" ]]; then
|
||||
GIT_ABSOLUTE="$VAULT/.git"
|
||||
else
|
||||
GIT_ABSOLUTE="$GIT_DIR"
|
||||
fi
|
||||
|
||||
HOOKS_DIR="$GIT_ABSOLUTE/hooks"
|
||||
HOOK_FILE="$HOOKS_DIR/pre-commit"
|
||||
HOOK_SOURCE="$SCRIPTS_DIR/okf-validate.sh"
|
||||
|
||||
# ─── Desinstalar ──────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "${1:-}" == "--uninstall" ]]; then
|
||||
if [[ -L "$HOOK_FILE" ]]; then
|
||||
rm "$HOOK_FILE"
|
||||
echo "[OK] Hook removido: $HOOK_FILE"
|
||||
elif [[ -f "$HOOK_FILE" ]]; then
|
||||
echo "[AVISO] $HOOK_FILE não é um symlink — remover manualmente se necessário"
|
||||
else
|
||||
echo "[INFO] Nenhum hook instalado"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── Instalar ─────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "=== Instalar OKF pre-commit hook ==="
|
||||
echo "Vault: $VAULT"
|
||||
echo "Git dir: $GIT_ABSOLUTE"
|
||||
echo "Hook: $HOOK_FILE"
|
||||
echo "Source: $HOOK_SOURCE"
|
||||
echo ""
|
||||
|
||||
# Verificar que o script de validação existe
|
||||
if [[ ! -f "$HOOK_SOURCE" ]]; then
|
||||
echo "ERRO: Script não encontrado — $HOOK_SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Tornar executável
|
||||
chmod +x "$HOOK_SOURCE"
|
||||
|
||||
# Criar diretório hooks se não existir
|
||||
mkdir -p "$HOOKS_DIR"
|
||||
|
||||
# Backup do hook existente (se não for nosso symlink)
|
||||
if [[ -f "$HOOK_FILE" ]] && [[ ! -L "$HOOK_FILE" ]]; then
|
||||
BACKUP="$HOOK_FILE.backup.$(date +%Y%m%d)"
|
||||
mv "$HOOK_FILE" "$BACKUP"
|
||||
echo "[INFO] Hook existente guardado em: $BACKUP"
|
||||
fi
|
||||
|
||||
# Remover symlink antigo se existir
|
||||
[[ -L "$HOOK_FILE" ]] && rm "$HOOK_FILE"
|
||||
|
||||
# Criar symlink
|
||||
ln -s "$HOOK_SOURCE" "$HOOK_FILE"
|
||||
chmod +x "$HOOK_FILE"
|
||||
|
||||
echo "[OK] Hook instalado: $HOOK_FILE → $HOOK_SOURCE"
|
||||
echo ""
|
||||
echo "Testar: git -C $VAULT commit --dry-run -m 'test'"
|
||||
echo "Validar tudo: bash $HOOK_SOURCE --all"
|
||||
echo "Desinstalar: bash $SCRIPTS_DIR/install-hooks.sh --uninstall"
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
okf-convert-wikilinks.py — Fase 3: Converte [[wikilinks]] → [texto](path.md) nos index.md
|
||||
OKF §5: links bundle-relative para navegação entre conceitos
|
||||
|
||||
Âmbito: apenas ficheiros index.md (navegação)
|
||||
Corpo de documentos (PROC, QR, etc.) mantém wikilinks — OKF tolera e Obsidian renderiza ambos.
|
||||
|
||||
Uso:
|
||||
python3 okf-convert-wikilinks.py [--dry-run] [--dir /path/to/Hub]
|
||||
|
||||
Criado: 28-06-2026
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HUB_DEFAULT = "/media/ealmeida/Dados/Hub"
|
||||
|
||||
EXCLUDE_DIRS = {".stversions", "node_modules", ".git", ".obsidian", ".trash"}
|
||||
|
||||
# Padrão wikilink: [[NomeFicheiro]] ou [[NomeFicheiro|Alias]]
|
||||
WIKILINK_RE = re.compile(r'\[\[([^\]|]+)(?:\|([^\]]+))?\]\]')
|
||||
|
||||
|
||||
def build_file_index(hub: Path) -> dict:
|
||||
"""Constrói índice nome→path para resolução de wikilinks."""
|
||||
index = {} # stem → Path relativo ao hub
|
||||
for root, dirs, files in os.walk(hub):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")]
|
||||
for fname in files:
|
||||
if fname.endswith(".md"):
|
||||
fp = Path(root) / fname
|
||||
stem = fp.stem.lower()
|
||||
rel = fp.relative_to(hub)
|
||||
# Guardar o primeiro match (mais provável no vault activo)
|
||||
if stem not in index:
|
||||
index[stem] = rel
|
||||
# Também indexar o nome completo sem extensão
|
||||
full_name = fname.lower()
|
||||
if full_name not in index:
|
||||
index[full_name] = rel
|
||||
return index
|
||||
|
||||
|
||||
def resolve_wikilink(target: str, current_file: Path, file_index: dict, hub: Path) -> str:
|
||||
"""Resolve [[target]] para um caminho relativo ao ficheiro actual."""
|
||||
# Limpar o target (remover ^anchor, #heading, etc.)
|
||||
target_clean = re.split(r'[#^]', target)[0].strip()
|
||||
target_lower = target_clean.lower()
|
||||
target_with_ext = target_lower + ".md" if not target_lower.endswith(".md") else target_lower
|
||||
|
||||
# Tentar resolver
|
||||
resolved = file_index.get(target_with_ext) or file_index.get(target_lower)
|
||||
|
||||
if resolved:
|
||||
# Calcular path relativo a partir do directório do ficheiro actual
|
||||
try:
|
||||
rel_path = os.path.relpath(hub / resolved, current_file.parent)
|
||||
return rel_path.replace("\\", "/")
|
||||
except Exception:
|
||||
return str(resolved)
|
||||
return None
|
||||
|
||||
|
||||
def convert_wikilinks_in_file(filepath: Path, file_index: dict, hub: Path, dry_run: bool) -> dict:
|
||||
"""Converte wikilinks no ficheiro. Retorna estatísticas."""
|
||||
result = {"file": str(filepath.relative_to(hub)), "converted": 0, "unresolved": [], "action": "skip"}
|
||||
|
||||
try:
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
result["action"] = "error"
|
||||
result["error"] = str(e)
|
||||
return result
|
||||
|
||||
if "[[" not in content:
|
||||
result["action"] = "no_wikilinks"
|
||||
return result
|
||||
|
||||
def replace_wikilink(m):
|
||||
target = m.group(1)
|
||||
alias = m.group(2)
|
||||
display = alias if alias else target
|
||||
|
||||
resolved_path = resolve_wikilink(target, filepath, file_index, hub)
|
||||
if resolved_path:
|
||||
result["converted"] += 1
|
||||
return f"[{display}]({resolved_path})"
|
||||
else:
|
||||
# Manter como wikilink se não resolvível
|
||||
result["unresolved"].append(target)
|
||||
return m.group(0)
|
||||
|
||||
new_content = WIKILINK_RE.sub(replace_wikilink, content)
|
||||
|
||||
if new_content != content:
|
||||
result["action"] = "converted"
|
||||
if not dry_run:
|
||||
filepath.write_text(new_content, encoding="utf-8")
|
||||
else:
|
||||
result["action"] = "no_changes"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
hub = Path(HUB_DEFAULT)
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith("--dir="):
|
||||
hub = Path(arg[6:])
|
||||
|
||||
if not hub.exists():
|
||||
print(f"ERRO: Hub não encontrado em {hub}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{'[DRY-RUN] ' if dry_run else ''}A construir índice de ficheiros…")
|
||||
file_index = build_file_index(hub)
|
||||
print(f" {len(file_index)} ficheiros indexados")
|
||||
|
||||
print(f"A converter wikilinks nos index.md…")
|
||||
total_converted = 0
|
||||
total_unresolved = []
|
||||
files_changed = 0
|
||||
|
||||
for root, dirs, files in os.walk(hub):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")]
|
||||
for fname in files:
|
||||
if fname != "index.md":
|
||||
continue
|
||||
filepath = Path(root) / fname
|
||||
result = convert_wikilinks_in_file(filepath, file_index, hub, dry_run)
|
||||
|
||||
if result["action"] == "converted":
|
||||
files_changed += 1
|
||||
total_converted += result["converted"]
|
||||
total_unresolved.extend(result["unresolved"])
|
||||
print(f" [OK] {result['file']}: {result['converted']} convertidos"
|
||||
+ (f", {len(result['unresolved'])} não resolvidos" if result["unresolved"] else ""))
|
||||
elif result["action"] == "error":
|
||||
print(f" [ERRO] {result['file']}: {result.get('error')}")
|
||||
|
||||
print(f"\n=== Resultado ===")
|
||||
print(f"Ficheiros alterados: {files_changed}")
|
||||
print(f"Wikilinks convertidos: {total_converted}")
|
||||
if total_unresolved:
|
||||
print(f"Não resolvidos ({len(total_unresolved)}): {', '.join(set(total_unresolved))[:200]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# okf-gen-graph.sh — Gera grafo OKF do Hub para integração com Wayland/visualização
|
||||
#
|
||||
# Uso:
|
||||
# bash scripts/okf-gen-graph.sh → gera hub-okf-graph.dot
|
||||
# bash scripts/okf-gen-graph.sh --svg → gera também hub-okf-graph.svg (requer graphviz)
|
||||
# bash scripts/okf-gen-graph.sh --info → mostra inventário do bundle
|
||||
#
|
||||
# Requer: okf CLI (cargo install --git https://github.com/W4G1/okf)
|
||||
#
|
||||
# Criado: 28-06-2026
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VAULT="/media/ealmeida/Dados/Hub"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUTPUT_DOT="$SCRIPT_DIR/hub-okf-graph.dot"
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[AVISO]${NC} $*"; }
|
||||
|
||||
if ! command -v okf &>/dev/null; then
|
||||
echo -e "${RED}[ERRO]${NC} okf CLI não encontrado."
|
||||
echo " Instalar: cargo install --git https://github.com/W4G1/okf"
|
||||
echo " Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Inventário do bundle
|
||||
if [[ "${1:-}" == "--info" ]]; then
|
||||
info "=== OKF Bundle Inventory ==="
|
||||
okf info "$VAULT" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Gerar grafo DOT
|
||||
info "A gerar grafo OKF do Hub..."
|
||||
okf graph "$VAULT" --dot 2>/dev/null > "$OUTPUT_DOT"
|
||||
NODE_COUNT=$(grep -c "^ " "$OUTPUT_DOT" 2>/dev/null || echo "?")
|
||||
ok "Grafo gerado: $OUTPUT_DOT ($NODE_COUNT nós/arestas)"
|
||||
|
||||
# Gerar SVG se graphviz disponível e --svg pedido
|
||||
if [[ "${1:-}" == "--svg" ]]; then
|
||||
OUTPUT_SVG="${OUTPUT_DOT%.dot}.svg"
|
||||
if command -v dot &>/dev/null; then
|
||||
info "A gerar SVG via graphviz..."
|
||||
dot -Tsvg "$OUTPUT_DOT" -o "$OUTPUT_SVG" 2>/dev/null
|
||||
ok "SVG gerado: $OUTPUT_SVG"
|
||||
info "Abrir com: xdg-open $OUTPUT_SVG"
|
||||
else
|
||||
warn "graphviz não instalado — só o DOT foi gerado"
|
||||
warn "Instalar: sudo apt install graphviz"
|
||||
warn "Ou visualizar online: https://dreampuf.github.io/GraphvizOnline/"
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Para Wayland F6: usar $OUTPUT_DOT como input de importação da estrutura Hub"
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
# okf-gen-logs.sh — Fase 4: Gera log.md por directório top-level
|
||||
# OKF §7: log.md com histórico de alterações por data ISO, newest first
|
||||
# Criado: 28-06-2026
|
||||
|
||||
HUB="/media/ealmeida/Dados/Hub"
|
||||
DAYS=90 # Últimos N dias de histórico
|
||||
DRY_RUN=false
|
||||
|
||||
if [[ "$1" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
echo "[DRY-RUN] Nenhum ficheiro será criado."
|
||||
fi
|
||||
|
||||
# Directórios top-level a processar (excluir .stversions, node_modules, tmp)
|
||||
TOP_DIRS=(
|
||||
"00-Inbox"
|
||||
"03-Propostas"
|
||||
"04-Stack"
|
||||
"05-Projectos"
|
||||
"06-Operacoes"
|
||||
"07-Clientes"
|
||||
"90-Templates"
|
||||
"99-Arquivo"
|
||||
)
|
||||
|
||||
SINCE=$(date -d "-${DAYS} days" +%Y-%m-%d)
|
||||
|
||||
generate_log() {
|
||||
local dir="$1"
|
||||
local dir_path="$HUB/$dir"
|
||||
local log_path="$dir_path/log.md"
|
||||
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
echo "[SKIP] $dir não existe"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "A gerar log.md para $dir (últimos ${DAYS} dias desde ${SINCE})…"
|
||||
|
||||
# Obter commits que tocaram nesta pasta
|
||||
local git_log
|
||||
git_log=$(git -C "$HUB" log \
|
||||
--since="$SINCE" \
|
||||
--format="%cd|%s" \
|
||||
--date=format:"%Y-%m-%d" \
|
||||
-- "$dir/" 2>/dev/null)
|
||||
|
||||
if [[ -z "$git_log" ]]; then
|
||||
echo " [INFO] Sem commits no período para $dir"
|
||||
git_log=""
|
||||
fi
|
||||
|
||||
# Agrupar por data e gerar markdown
|
||||
local log_content
|
||||
log_content="# Log de Actualizações — $dir
|
||||
|
||||
$(echo "$git_log" | awk -F'|' '
|
||||
{
|
||||
date=$1; msg=$2
|
||||
if (date != prev_date) {
|
||||
if (prev_date != "") print ""
|
||||
print "## " date
|
||||
prev_date=date
|
||||
}
|
||||
# Classificar entrada
|
||||
if (msg ~ /^(feat|add|create|novo|cria)/) prefix="**Creation**"
|
||||
else if (msg ~ /^(fix|corr|resolv)/) prefix="**Fix**"
|
||||
else if (msg ~ /^(archive|arquiv)/) prefix="**Archive**"
|
||||
else if (msg ~ /^(delete|remov|apag)/) prefix="**Deletion**"
|
||||
else prefix="**Update**"
|
||||
print "* " prefix ": " msg
|
||||
}' 2>/dev/null || echo "_(sem histórico git no período)_")
|
||||
"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY] $log_path"
|
||||
echo " Primeiras linhas: $(echo "$log_content" | head -5)"
|
||||
else
|
||||
echo "$log_content" > "$log_path"
|
||||
echo " [OK] $log_path"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== okf-gen-logs.sh — $(date -I) ==="
|
||||
echo "Hub: $HUB"
|
||||
echo ""
|
||||
|
||||
for dir in "${TOP_DIRS[@]}"; do
|
||||
generate_log "$dir"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Concluído ==="
|
||||
echo "log.md gerado em ${#TOP_DIRS[@]} directórios."
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
okf-normalize.py — Fase 1: Normaliza frontmatter OKF em todos os .md do Hub
|
||||
Adiciona/completa: type, title, description, timestamp
|
||||
OKF SPEC §4.1: type é o único campo obrigatório
|
||||
|
||||
Uso:
|
||||
python3 okf-normalize.py [--dry-run] [--dir /path/to/Hub]
|
||||
|
||||
Criado: 28-06-2026
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HUB_DEFAULT = "/media/ealmeida/Dados/Hub"
|
||||
|
||||
# Directórios excluídos do scan
|
||||
EXCLUDE_DIRS = {
|
||||
".stversions",
|
||||
"node_modules",
|
||||
".git",
|
||||
".obsidian",
|
||||
".trash",
|
||||
"99-Arquivo",
|
||||
}
|
||||
|
||||
# Ficheiros reservados OKF — sem frontmatter obrigatório
|
||||
OKF_RESERVED = {"index.md", "log.md"}
|
||||
|
||||
# Taxonomia Hub → OKF type
|
||||
def infer_type(filepath: Path) -> str:
|
||||
name = filepath.name
|
||||
parts = str(filepath).lower()
|
||||
|
||||
if name.startswith("PROC-") or name.startswith("proc-"):
|
||||
return "Playbook"
|
||||
if name.startswith("QR-") or name.startswith("qr-"):
|
||||
return "Reference"
|
||||
if name.lower() in ("index.md", "index.md"):
|
||||
return "Index"
|
||||
if name.endswith("-SPEC.md") or name == "SPEC.md":
|
||||
return "Specification"
|
||||
if name.startswith("STATUS"):
|
||||
return "Status"
|
||||
if name.upper().startswith("CHANGELOG"):
|
||||
return "Changelog"
|
||||
if name.upper().startswith("README"):
|
||||
return "Reference"
|
||||
if "proposta" in parts or "orcamento" in parts or "budget" in parts:
|
||||
return "Proposal"
|
||||
if "90-templates" in parts or "/template" in parts:
|
||||
return "Template"
|
||||
if "07-clientes" in parts:
|
||||
return "Client Profile"
|
||||
return "Document"
|
||||
|
||||
|
||||
def get_git_timestamp(filepath: Path, hub: Path) -> str:
|
||||
"""Obter timestamp da última modificação via git log."""
|
||||
try:
|
||||
rel = filepath.relative_to(hub)
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--format=%cI", "--", str(rel)],
|
||||
cwd=str(hub),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
ts = result.stdout.strip()
|
||||
if ts:
|
||||
return ts
|
||||
except Exception:
|
||||
pass
|
||||
# fallback: mtime do ficheiro
|
||||
mtime = filepath.stat().st_mtime
|
||||
return datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
|
||||
|
||||
|
||||
def parse_frontmatter(content: str):
|
||||
"""Retorna (frontmatter_str, body_str, has_fm) ou (None, content, False)."""
|
||||
if content.startswith("---\n"):
|
||||
end = content.find("\n---\n", 4)
|
||||
if end != -1:
|
||||
fm = content[4:end]
|
||||
body = content[end + 5:]
|
||||
return fm, body, True
|
||||
return None, content, False
|
||||
|
||||
|
||||
def first_useful_sentence(body: str) -> str:
|
||||
"""Extrai primeira frase útil do body para description."""
|
||||
# Remover headings, listas, blocos de código
|
||||
lines = body.split("\n")
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if line.startswith("```"):
|
||||
continue
|
||||
if line.startswith("|"):
|
||||
continue
|
||||
if line.startswith("-") or line.startswith("*"):
|
||||
# Lista: usar conteúdo sem bullet
|
||||
line = re.sub(r"^[-*]\s+", "", line)
|
||||
# Limpar markdown inline
|
||||
line = re.sub(r"\*\*(.+?)\*\*", r"\1", line)
|
||||
line = re.sub(r"\[(.+?)\]\(.+?\)", r"\1", line)
|
||||
line = line.strip()
|
||||
if len(line) > 10:
|
||||
# Truncar em 120 chars
|
||||
return line[:120].rstrip(".") + ("…" if len(line) > 120 else "")
|
||||
return ""
|
||||
|
||||
|
||||
def normalize_file(filepath: Path, hub: Path, dry_run: bool) -> dict:
|
||||
"""Normaliza um ficheiro. Retorna dict com acção tomada."""
|
||||
result = {"file": str(filepath.relative_to(hub)), "action": "skip", "changes": []}
|
||||
|
||||
try:
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
result["action"] = "error"
|
||||
result["error"] = str(e)
|
||||
return result
|
||||
|
||||
fm_str, body, has_fm = parse_frontmatter(content)
|
||||
|
||||
if not has_fm:
|
||||
# Injetar frontmatter mínimo
|
||||
inferred_type = infer_type(filepath)
|
||||
title = filepath.stem.replace("-", " ").replace("_", " ").title()
|
||||
description = first_useful_sentence(body)
|
||||
timestamp = get_git_timestamp(filepath, hub)
|
||||
|
||||
new_fm_lines = [f"type: {inferred_type}", f"title: {title}"]
|
||||
if description:
|
||||
new_fm_lines.append(f"description: >-\n {description}")
|
||||
new_fm_lines.append(f"timestamp: {timestamp}")
|
||||
|
||||
new_content = "---\n" + "\n".join(new_fm_lines) + "\n---\n" + content
|
||||
result["action"] = "add_frontmatter"
|
||||
result["changes"] = new_fm_lines
|
||||
else:
|
||||
# Ficheiro já tem frontmatter — completar campos em falta
|
||||
fm_lines = fm_str.split("\n")
|
||||
changes = []
|
||||
|
||||
has_type = any(line.startswith("type:") for line in fm_lines)
|
||||
has_title = any(line.startswith("title:") for line in fm_lines)
|
||||
has_description = any(line.startswith("description:") for line in fm_lines)
|
||||
has_timestamp = any(
|
||||
line.startswith("timestamp:") or line.startswith("date:")
|
||||
for line in fm_lines
|
||||
)
|
||||
|
||||
if not has_type:
|
||||
inferred_type = infer_type(filepath)
|
||||
fm_lines.insert(0, f"type: {inferred_type}")
|
||||
changes.append(f"+ type: {inferred_type}")
|
||||
|
||||
if not has_title:
|
||||
title = filepath.stem.replace("-", " ").replace("_", " ").title()
|
||||
# Inserir após type
|
||||
type_idx = next(
|
||||
(i for i, l in enumerate(fm_lines) if l.startswith("type:")), 0
|
||||
)
|
||||
fm_lines.insert(type_idx + 1, f"title: {title}")
|
||||
changes.append(f"+ title: {title}")
|
||||
|
||||
if not has_description:
|
||||
desc = first_useful_sentence(body)
|
||||
if desc:
|
||||
desc_entry = f"description: >-\n {desc}"
|
||||
title_idx = next(
|
||||
(i for i, l in enumerate(fm_lines) if l.startswith("title:")), 1
|
||||
)
|
||||
fm_lines.insert(title_idx + 1, desc_entry)
|
||||
changes.append(f"+ description: {desc[:60]}…")
|
||||
|
||||
if not has_timestamp:
|
||||
ts = get_git_timestamp(filepath, hub)
|
||||
fm_lines.append(f"timestamp: {ts}")
|
||||
changes.append(f"+ timestamp: {ts}")
|
||||
|
||||
if not changes:
|
||||
result["action"] = "already_ok"
|
||||
return result
|
||||
|
||||
new_fm = "\n".join(fm_lines)
|
||||
new_content = "---\n" + new_fm + "\n---\n" + body
|
||||
result["action"] = "update_frontmatter"
|
||||
result["changes"] = changes
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
filepath.write_text(new_content, encoding="utf-8")
|
||||
except Exception as e:
|
||||
result["action"] = "error"
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def scan_hub(hub: Path, dry_run: bool):
|
||||
"""Scan recursivo do vault Hub."""
|
||||
stats = {"add": 0, "update": 0, "ok": 0, "skip": 0, "error": 0}
|
||||
report_lines = [
|
||||
f"# okf-normalize — {'DRY-RUN' if dry_run else 'EXECUÇÃO'} — {datetime.now().isoformat()[:16]}",
|
||||
f"Hub: {hub}",
|
||||
"",
|
||||
]
|
||||
|
||||
for root, dirs, files in os.walk(hub):
|
||||
root_path = Path(root)
|
||||
|
||||
# Excluir directórios
|
||||
dirs[:] = [
|
||||
d for d in dirs
|
||||
if d not in EXCLUDE_DIRS and not d.startswith(".")
|
||||
]
|
||||
|
||||
for fname in files:
|
||||
if not fname.endswith(".md"):
|
||||
continue
|
||||
if fname.lower() in OKF_RESERVED:
|
||||
continue
|
||||
|
||||
filepath = root_path / fname
|
||||
result = normalize_file(filepath, hub, dry_run)
|
||||
|
||||
action = result["action"]
|
||||
if action == "add_frontmatter":
|
||||
stats["add"] += 1
|
||||
report_lines.append(f"[ADD] {result['file']}")
|
||||
for c in result["changes"]:
|
||||
report_lines.append(f" {c}")
|
||||
elif action == "update_frontmatter":
|
||||
stats["update"] += 1
|
||||
report_lines.append(f"[UPD] {result['file']}")
|
||||
for c in result["changes"]:
|
||||
report_lines.append(f" {c}")
|
||||
elif action == "already_ok":
|
||||
stats["ok"] += 1
|
||||
elif action == "error":
|
||||
stats["error"] += 1
|
||||
report_lines.append(f"[ERR] {result['file']}: {result.get('error')}")
|
||||
else:
|
||||
stats["skip"] += 1
|
||||
|
||||
report_lines += [
|
||||
"",
|
||||
"## Resultado",
|
||||
f"- Frontmatter adicionado: {stats['add']}",
|
||||
f"- Frontmatter actualizado: {stats['update']}",
|
||||
f"- Já conformes: {stats['ok']}",
|
||||
f"- Erros: {stats['error']}",
|
||||
f"- Ignorados: {stats['skip']}",
|
||||
]
|
||||
return stats, "\n".join(report_lines)
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
hub = Path(HUB_DEFAULT)
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith("--dir="):
|
||||
hub = Path(arg[6:])
|
||||
|
||||
if not hub.exists():
|
||||
print(f"ERRO: Hub não encontrado em {hub}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{'[DRY-RUN] ' if dry_run else ''}A normalizar OKF em {hub}…")
|
||||
stats, report = scan_hub(hub, dry_run)
|
||||
|
||||
report_path = hub / "04-Stack/02.04-Sistemas/MemoriaCentral/scripts/okf-normalize-report.md"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
|
||||
print(report_path.read_text(encoding="utf-8").split("## Resultado")[1].strip())
|
||||
print(f"\nRelatório completo: {report_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# okf-rename-index.sh — Fase 2: Renomeia INDEX.md → index.md no vault Hub
|
||||
# OKF §6: index.md é ficheiro reservado (lowercase)
|
||||
# Criado: 28-06-2026
|
||||
|
||||
HUB="/media/ealmeida/Dados/Hub"
|
||||
DRY_RUN=false
|
||||
LOG_FILE="$(dirname "$0")/okf-rename-index.log"
|
||||
|
||||
# Modo dry-run com --dry-run
|
||||
if [[ "$1" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
echo "[DRY-RUN] Nenhum ficheiro será alterado."
|
||||
fi
|
||||
|
||||
echo "=== okf-rename-index.sh — $(date -I) ===" | tee "$LOG_FILE"
|
||||
echo "Hub: $HUB" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
|
||||
COUNT=0
|
||||
ERRORS=0
|
||||
|
||||
# Encontrar todos os INDEX.md excluindo .stversions e node_modules
|
||||
while IFS= read -r -d '' INDEX_FILE; do
|
||||
DIR=$(dirname "$INDEX_FILE")
|
||||
TARGET="$DIR/index.md"
|
||||
|
||||
# Verificar se já existe index.md (colisão)
|
||||
if [[ -f "$TARGET" ]]; then
|
||||
echo "[SKIP] Colisão: $TARGET já existe — manter INDEX.md" | tee -a "$LOG_FILE"
|
||||
((ERRORS++))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[DRY] $INDEX_FILE → $TARGET" | tee -a "$LOG_FILE"
|
||||
else
|
||||
# Usar git mv para preservar histórico
|
||||
if git -C "$HUB" mv "${INDEX_FILE#$HUB/}" "${TARGET#$HUB/}" 2>>"$LOG_FILE"; then
|
||||
echo "[OK] $INDEX_FILE → $TARGET" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "[ERRO] Falha: $INDEX_FILE" | tee -a "$LOG_FILE"
|
||||
((ERRORS++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
((COUNT++))
|
||||
|
||||
done < <(find "$HUB" -name "INDEX.md" \
|
||||
-not -path "*/.stversions/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/99-Arquivo/*" \
|
||||
-print0)
|
||||
|
||||
# Incluir 99-Arquivo separadamente (sem git mv — só rename simples)
|
||||
while IFS= read -r -d '' INDEX_FILE; do
|
||||
DIR=$(dirname "$INDEX_FILE")
|
||||
TARGET="$DIR/index.md"
|
||||
|
||||
if [[ -f "$TARGET" ]]; then
|
||||
echo "[SKIP] Colisão: $TARGET já existe" | tee -a "$LOG_FILE"
|
||||
((ERRORS++))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[DRY-ARQUIVO] $INDEX_FILE → $TARGET" | tee -a "$LOG_FILE"
|
||||
else
|
||||
if mv "$INDEX_FILE" "$TARGET" 2>>"$LOG_FILE"; then
|
||||
echo "[OK-ARQUIVO] $INDEX_FILE → $TARGET" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "[ERRO-ARQUIVO] $INDEX_FILE" | tee -a "$LOG_FILE"
|
||||
((ERRORS++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
((COUNT++))
|
||||
|
||||
done < <(find "$HUB/99-Arquivo" -name "INDEX.md" \
|
||||
-not -path "*/.stversions/*" \
|
||||
-print0)
|
||||
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "=== Resultado ===" | tee -a "$LOG_FILE"
|
||||
echo "Renomeados: $COUNT" | tee -a "$LOG_FILE"
|
||||
echo "Erros/Colisoes: $ERRORS" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
|
||||
if [[ "$DRY_RUN" == "false" && $COUNT -gt 0 ]]; then
|
||||
echo "=== Actualizar referencias internas ===" | tee -a "$LOG_FILE"
|
||||
# Substituir [INDEX.md] e (INDEX.md) por index.md nas referencias
|
||||
grep -rl "INDEX\.md" "$HUB" \
|
||||
--include="*.md" \
|
||||
--exclude-dir=".stversions" \
|
||||
--exclude-dir="node_modules" | while read -r FILE; do
|
||||
sed -i 's/\bINDEX\.md\b/index.md/g' "$FILE"
|
||||
echo "[REF] $FILE" >> "$LOG_FILE"
|
||||
done
|
||||
echo "Referencias actualizadas — ver log para detalhes." | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "Log: $LOG_FILE"
|
||||
Executable
+352
@@ -0,0 +1,352 @@
|
||||
#!/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}
|
||||
Reference in New Issue
Block a user