Compare commits
3 Commits
6035542b67
...
4d3583b81a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d3583b81a | |||
| b78cc9c465 | |||
| 8d4988ad3f |
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Classifica layer raw|wiki nos .md do Cloud (estrutura cliente/pessoal/empresa)."""
|
||||
import sys, re, os
|
||||
from pathlib import Path
|
||||
ROOT = Path("/media/ealmeida/Dados/Cloud")
|
||||
DRY = "--dry-run" in sys.argv
|
||||
|
||||
EPHEMERAL = re.compile(r'(worklog|checkup|sess[aã]o|di[aá]rio|reuni[aã]o|/logs?/|fontes|pesquisa|deep-research|relat[oó]rio-di[aá]rio)', re.I)
|
||||
|
||||
def classify(rel: str) -> str:
|
||||
p = rel.lower()
|
||||
if EPHEMERAL.search(p): return "raw"
|
||||
# armazenamento de cliente/pessoal = raw (lê-se, não se cura como grafo)
|
||||
if p.startswith(("clientes_360/", "clientes/", "eal/", "tomplays/", "docsdrive/")): return "raw"
|
||||
# conhecimento de empresa/admin = wiki
|
||||
if p.startswith(("descomplicar/", "adm_descomplicar/", "adm_missão_pertinente/")): return "wiki"
|
||||
return "wiki" # defeito
|
||||
|
||||
add=upd=0
|
||||
for f in ROOT.rglob("*.md"):
|
||||
rel = str(f.relative_to(ROOT))
|
||||
if any(part.startswith(".") for part in f.parts) or "node_modules" in rel: continue
|
||||
if f.name in ("index.md","log.md"): continue
|
||||
try: txt = f.read_text(encoding="utf-8")
|
||||
except: continue
|
||||
if not txt.startswith("---"): continue
|
||||
end = txt.find("\n---", 3)
|
||||
if end < 0: continue
|
||||
fm = txt[3:end]
|
||||
if re.search(r'^layer:', fm, re.M): continue # já tem
|
||||
layer = classify(rel)
|
||||
new = txt[:end] + f"\nlayer: {layer}" + txt[end:]
|
||||
if not DRY: f.write_text(new, encoding="utf-8")
|
||||
add+=1
|
||||
print(f"{'[DRY] ' if DRY else ''}layer adicionado a {add} ficheiros")
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/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" == 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)"
|
||||
@@ -81,14 +81,46 @@ def get_git_timestamp(filepath: Path, hub: Path) -> str:
|
||||
return datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _looks_like_yaml_mapping(fm: str) -> bool:
|
||||
"""True só se o bloco for um mapa YAML válido (evita tratar régua '---' + corpo
|
||||
como frontmatter — causa de corrupção em MEMORY.md e afins)."""
|
||||
if not fm.strip():
|
||||
return False
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
data = yaml.safe_load(fm)
|
||||
return isinstance(data, dict)
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
return False
|
||||
# Fallback sem PyYAML: a 1ª linha não-vazia tem de ser uma chave `nome:`;
|
||||
# nenhuma linha de topo (não-indentada, não-vazia) pode ser heading markdown.
|
||||
first = None
|
||||
for ln in fm.split("\n"):
|
||||
if not ln.strip():
|
||||
continue
|
||||
if first is None:
|
||||
first = ln
|
||||
if ln[:1] == "#": # heading markdown ao nível de topo
|
||||
return False
|
||||
if first is None:
|
||||
return False
|
||||
return bool(re.match(r"^[A-Za-z_][\w\-]*:", first))
|
||||
|
||||
|
||||
def parse_frontmatter(content: str):
|
||||
"""Retorna (frontmatter_str, body_str, has_fm) ou (None, content, False)."""
|
||||
"""Retorna (frontmatter_str, body_str, has_fm) ou (None, content, False).
|
||||
|
||||
Só considera frontmatter um bloco `---…---` inicial que seja um MAPA YAML válido.
|
||||
Um `---` inicial usado como régua/separador (seguido de corpo) NÃO é frontmatter."""
|
||||
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
|
||||
if _looks_like_yaml_mapping(fm):
|
||||
body = content[end + 5:]
|
||||
return fm, body, True
|
||||
return None, content, False
|
||||
|
||||
|
||||
@@ -280,7 +312,7 @@ def main():
|
||||
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 = Path(__file__).parent / "okf-normalize-report.md"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
|
||||
print(report_path.read_text(encoding="utf-8").split("## Resultado")[1].strip())
|
||||
|
||||
+240
-321
@@ -1,352 +1,271 @@
|
||||
#!/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")"
|
||||
[[ "$base" == MEMORY-*.md ]] && return 0 # variantes MEMORY legacy
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user