Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions .claude/skills/check-module/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check-module
description: Valida localmente se o module.json e arquivos derivados estao em conformidade com o contrato do Shell API. Roda em segundos e pega os erros mais comuns (defaults nao personalizados, mutual exclusivity nav/perspectives, paths com drift, perms orfas, symlink do chart faltando) antes de o registro falhar no deploy.
description: Valida localmente se o module.json e arquivos derivados estao em conformidade com o contrato do Shell API. Roda em segundos e pega os erros mais comuns (defaults nao personalizados, mutual exclusivity nav/perspectives, paths com drift, formato de permissao invalido, perms orfas, symlink do chart faltando) antes de o registro falhar no deploy.
user-invocable: true
disable-model-invocation: false
allowed-tools: Read, Glob, Grep, Bash
Expand Down Expand Up @@ -113,7 +113,62 @@ while IFS= read -r p; do
done <<< "$PATHS"
```

### 2.6 — Permissoes referenciadas existem em `permissions[]` (severity: warn)
### 2.6 — Formato das permissoes (severity: error)

O shell parsa cada string em `permissions[]` (e `requiredPermission` em nav/perspectives) com `indexOf(":")` — divide no **primeiro** colon. Isso significa que strings malformadas podem parsar e ainda assim virar permissao morta: o registro passa, o modulo aparece, mas nenhuma role do RBAC bate a "acao" resultante. O dev so descobre quando algum usuario reclama que nao consegue acessar.

Formato correto: `<module-id>:<acao>` — exatamente um colon, recurso igual ao id do modulo, acao em kebab-case simples (`[a-z][a-z0-9_-]*`).

Armadilhas tipicas que parsam mas nao funcionam:

- `my-module:conta:read` — extra segment. Parser produz `resource=my-module, action="conta:read"`. Nenhuma role concede `"conta:read"`. **Esse e o caso que motivou este check.**
- `outro-modulo:read` num modulo cujo id e `my-module` — declarando ownership de outro modulo.
- `MyModule:Read` — RBAC e case-sensitive, nunca casa com a role.
- `my-module:` ou `:read` — segmento vazio, parser retorna `null`.

Roda **antes** de 2.7 pra que o orphan check ali nao gere ruido sobre strings ja sabidamente quebradas.

```bash
validate_perm_format() {
local label="$1" perm="$2"
[ -z "$perm" ] && return
local colons resource action
colons=$(awk -F: '{print NF-1}' <<< "$perm")
if [ "$colons" -eq 0 ]; then
PROBLEMS+=("erro: $label \"$perm\" sem ':' — formato e \"$ID:<acao>\"")
return
fi
if [ "$colons" -gt 1 ]; then
PROBLEMS+=("erro: $label \"$perm\" tem $colons ':' — formato e \"$ID:<acao>\", nao \"$ID:<sub>:<acao>\"")
return
fi
resource="${perm%%:*}"
action="${perm#*:}"
if [ -z "$resource" ] || [ -z "$action" ]; then
PROBLEMS+=("erro: $label \"$perm\" tem segmento vazio")
return
fi
if [ "$resource" != "$ID" ]; then
PROBLEMS+=("erro: $label \"$perm\" usa recurso \"$resource\" diferente do id do modulo \"$ID\"")
return
fi
if ! [[ "$action" =~ ^[a-z][a-z0-9_-]*$ ]]; then
PROBLEMS+=("aviso: $label \"$perm\" — acao \"$action\" foge do kebab-case [a-z][a-z0-9_-]*; RBAC e case-sensitive")
fi
}

# Declared (permissions raiz)
while IFS= read -r p; do validate_perm_format "permissao" "$p"; done <<< \
"$(jq -r '(.permissions // []) | if type == "array" then .[] else empty end' module.json)"

# Used (requiredPermission em nav/perspectives) — mesmo check, label diferente
while IFS= read -r p; do validate_perm_format "requiredPermission" "$p"; done <<< \
"$(jq -r '((.navigation // []) + (.perspectives // [] | map(.navigation // []) | add // [])) | .[] | .requiredPermission // empty' module.json)"
```

> Por que erro e nao aviso, ja que o shell aceita a string? Porque o sintoma e silencioso. Registro funciona, sidebar funciona — so o RBAC nunca casa. Falha invisivel em ambiente vale mais ser barrada localmente.

### 2.7 — Permissoes referenciadas existem em `permissions[]` (severity: warn)

Cada `requiredPermission` usada em `navigation`/`perspectives` deve estar declarada no array `permissions` raiz. Se o nav usa `module:approve` mas `permissions[]` so tem `module:read`, e bug certo (RBAC nao vai conseguir conceder).

Expand All @@ -130,7 +185,7 @@ ORPHANS=$(comm -23 <(echo "$USED") <(echo "$DECLARED"))

Cada linha em `ORPHANS` vira warn. Se `permissions` nao for array, isso ja vira error em 2.2 — aqui so evitamos abortar a execucao.

### 2.7 — Symlink do chart (severity: error)
### 2.8 — Symlink do chart (severity: error)

Se existe `charts/$ID/`, deve existir `charts/$ID/module.json` como **symlink** para `../../module.json`. Sem isso, o registration ConfigMap renderiza `module.json` errado em deploy (o chart le via `.Files.Get`).

Expand All @@ -144,11 +199,11 @@ if [ -d "charts/$ID" ]; then
fi
```

### 2.8 — `platform.shellApiUrl` configurada (severity: warn)
### 2.9 — `platform.shellApiUrl` configurada (severity: warn)

Necessario para `platform-setup` rodar. Se vazio ou ainda contem `stage.cora.team` num modulo prod, sinalize.

### 2.9 — `accent` no enum V2 (severity: error quando declarado, warn quando ausente)
### 2.10 — `accent` no enum V2 (severity: error quando declarado, warn quando ausente)

O shell aceita 7 valores fechados pro `accent`. Se declarado fora desse set, o `POST /api/modules` rejeita. Se ausente, o shell auto-infere via hash deterministico do `id` — sem custo, mas vale informar pro dev:

Expand All @@ -167,7 +222,7 @@ case "$ACCENT" in
esac
```

### 2.10 — `icon` ausente (severity: warn)
### 2.11 — `icon` ausente (severity: warn)

`icon` e nome lucide em kebab-case. Sem ele, o shell usa o default do `group`. Se ambos `accent` e `icon` ausentes, modulos do mesmo grupo ficam visualmente indistinguiveis na grid:

Expand All @@ -178,7 +233,7 @@ if [ -z "$ICON" ]; then
fi
```

### 2.11 — Hardcoded colors no `src/` (severity: warn)
### 2.12 — Hardcoded colors no `src/` (severity: warn)

Modulos devem consumir CSS custom properties (`var(--token)`) em vez de fixar hex. Cores fixas escapam quando o shell troca de tema (light/dark) ou quando o palette V2 evolui.

Expand Down
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ To switch pages inside the module, use `useModuleRoute(moduleManifest.id)` ([src

Use `Tabs` from `@/components/ui/tabs` **only** for tightly-coupled sub-views inside a single page. Distinct pages always belong in `navigation[]`.

## Permission strings — exactly two segments

Every string in `permissions[]` and every `requiredPermission` in nav/perspectives follows **`<module-id>:<action>`**. One colon. The module-id segment must equal this module's `id`; the action is kebab-case (`[a-z][a-z0-9_-]*`). The shell parses with `indexOf(":")` (splits on the first colon), so extra segments silently collapse into a malformed action that **no RBAC role can ever grant** — the module registers, the sidebar renders, and the user gets a permanent permission-denied with no error in any log.

```jsonc
// ✅ correct
"permissions": ["my-module:read", "my-module:write", "my-module:approve"]

// ❌ wrong — three segments. Parser produces action="conta:read". Dead permission.
"permissions": ["my-module:conta:read"]

// ❌ wrong — resource segment differs from module id.
"permissions": ["outro-modulo:read"]

// ❌ wrong — RBAC is case-sensitive.
"permissions": ["MyModule:Read"]
```

If you need to scope by sub-resource, encode it in the action with `-` or `_`, not `:` — e.g. `my-module:conta-read`, `my-module:invoice_approve`. `/check-module` enforces this format and treats violations as errors.

## ShellContext API

```typescript
Expand Down