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