[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C0TPMAS2DJ93AWST0R4GVV":3,"skill-same:01M6C0TPMAS2DJ93AWST0R4GVV":83},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":14,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":49,"updatedAt":50,"install":51,"resources":64},"01M6C0TPMAS2DJ93AWST0R4GVV","aprender-domain",null,"Business-rules reference for Aprender Sistema v2 — RF (functional requirements), RD (availability rules), PA (approval policy), CP (immutable clauses), and core domain models. Use when implementing a feature, validating a requirement, or writing a compliance test against any RF\u002FRD\u002FPA\u002FCP rule.","matheusnorjosa\u002Faprender_sistema","matheusnorjosa","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F117673804?v=4","01M6C0S19FG12BDFBTRWY9TTEQ","Matheus Norjosa","01M6C0S1DF0YB17TKGEYC70GSP",0,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"domain","aprender domain","feature","when","models","cp","functional","requirements","business","use","implementing","for","availability","policy","core","compliance","Rules","writing","immutable","test","2026-11-10 10:12:30",false,"---\nname: aprender-domain\ndescription: Business-rules reference for Aprender Sistema v2 — RF (functional requirements), RD (availability rules), PA (approval policy), CP (immutable clauses), and core domain models. Use when implementing a feature, validating a requirement, or writing a compliance test against any RF\u002FRD\u002FPA\u002FCP rule.\n---\n\n# Aprender Sistema v2 — Business Domain\n\nReference for the stable domain contracts (RF \u002F RD \u002F PA \u002F CP) and core models.\n\n> **SSOT dos contratos vivos**: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002F*.spec.md` (modelo SDD, ADR-017).\n> Cada spec tem `status` + `last_verified` + `sources_of_truth`. Não duplicar métricas\n> datadas aqui; este skill resume a regra e aponta para a spec viva.\n\n## CP (Cláusulas Pétreas) — IMMUTABLE\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fclausulas-petreas.spec.md` (CP-01..CP-08).\n\n- **CP-01**: v2 roda APENAS em Docker (`cd v2 && make up`). Validação `REQUIRE_DOCKER` em `config\u002Fsettings.py`.\n- **CP-02**: PA-01..PA-07 — aprovação manual obrigatória (ver PA abaixo).\n- **CP-03**: RD-01..RD-08 — disponibilidade com timezone Fortaleza (ver RD abaixo).\n- **CP-04**: Workflow Entender → Planejar → Implementar → Testar. Nunca pular etapas.\n- **CP-05** a **CP-08**: v1 congelado; conventional commits; nunca push direto na main (hook); `INCLUDE_DEV_TOOLS=false` em produção.\n\n## RD (Regras de Disponibilidade) — Availability Rules\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fregras-disponibilidade.spec.md`.\nImplementação: `apps\u002Fcore\u002Fservices\u002Favailability_service.py:check_conflicts()`.\nTestes: `apps\u002Fcore\u002Ftests\u002Ftest_availability_service.py`.\n\n- **RD-01 — Non-overlapping**: quem ocupa a agenda (`ENFORCED_ROLES`: FORMADOR e, desde 03\u002F11\u002F2026, COORD_ACOMPANHA; o coordenador responsável só com `coordenador_acompanha=True`; ver `ocupa_agenda_q`) não pode ter dois eventos que se sobreponham (≥1 min). `end == start` é adjacente, NÃO é conflito.\n- **RD-02 — Bloqueio total (T)**: bloqueio marcado `T` impede QUALQUER evento no intervalo. Código `T`.\n- **RD-03 — Bloqueio parcial (P)**: bloqueio `P` impede eventos só dentro do subintervalo; fora é permitido.\n- **RD-04 — Deslocamento (D)**: entre municípios diferentes exige tempo mínimo de viagem (configurável). Mesmo município pode ser zero.\n- **RD-05 — Teto de horas por evento (contagem de CH)**: desde 05\u002F10\u002F2026 (decisão do dono, OK explícito para mudar RD-05\u002FCP-03) **não é checagem de agenda**: o motor não emite `M` em nenhum caminho (de 02\u002F10 a 05\u002F10 foi aviso; antes barrava). O parâmetro `AVAILABILITY_DAILY_LIMIT_HOURS` (Configurações, \"Teto de horas por evento (contagem)\", 1 a 12, padrão 8) é o teto da contagem de horas de formação: CH de um evento = `min(fim − início, teto)`; as CH do dia somam (sem teto por dia); evento que cruza a meia-noite conta no dia local do início; sem desconto de almoço; online conta; só aprovado, qualquer projeto; conta sempre quem ocupa a agenda pelo papel (`ENFORCED_ROLES` = FORMADOR e, desde 03\u002F11\u002F2026, COORD_ACOMPANHA, os coordenadores além do responsável); o coordenador responsável (`coordenador`; sem ele, quem criou) só com `coordenador_acompanha=True`; convidado não conta; a mesma pessoa conta uma vez por evento. SSOT `services\u002Fhoras_formacao.py` (`ocupacoes_aprovadas`: Grade Mensal `ch_month`\u002F`ch_year`, painel de Equipe `horas_trabalhadas` e a população do ranking de formadores).\n- **RD-06 — Timezone**: comparações timezone-aware em `America\u002FFortaleza`; armazenamento em UTC.\n- **RD-07 — Ordem dos checks**: (1) blocos T\u002FP → (2) conflitos de eventos aprovados → (3) deslocamento D. Todos barram (`conflicts`); `warnings` só existe na checagem prévia (`check\u002F`, `check-many\u002F`) e vem vazio desde 05\u002F10\u002F2026 (saiu do guard e do 400 em 06\u002F10\u002F2026).\n- **RD-08 — Mensagens de conflito**: cada `Conflict` carrega `code`, `title`, `detail` (formador e intervalo `HH:MM dd\u002Fmm`) e `ref_id` opcional. Os códigos emitidos são `X`, `T`, `P` e `D`; não há mensagem de limite diário (o RD-05 é contagem de horas, não conflito).\n\n```json\n{ \"code\": \"T\", \"title\": \"Bloqueio total\", \"detail\": \"Maria Silva - 15\u002F01\u002F2025 09:00-12:00\", \"ref_id\": 123 }\n```\n\n## PA (Política de Aprovação) — Approval Policy\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md`.\nTestes: `apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py`.\n\n- **PA-01 — No auto-approval (SUPER)**: `Solicitacao` com `projeto.fluxo == 'SUPER'` NUNCA vira \"aprovado\" automaticamente, mesmo sem conflito. Decidido por `resolve_initial_status()` (`apps\u002Fcore\u002Fservices\u002Fsolicitacao_create.py:23`), chamado no `perform_create` do `SolicitacaoViewSet` (`views_solicitacao.py`) — **não** há override de `Solicitacao.save()`.\n- **PA-02 — Perfil exigido**: regra única `solicitation_approval_basis`; ver PA-02 em `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md` (nenhum grupo aprova). Idioma canônico `permission_classes = [HasPerm(\"codename\")]` (`from apps.core.rbac import HasPerm`; políticas em `apps\u002Fcore\u002Frbac\u002F`).\n- **PA-03 — Triggers pós-aprovação**: integrações externas (GCal, RF05\u002FRF06) só executam APÓS a aprovação manual; a task de publicação não é chamada na criação (fluxo service-layer: `solicitacao_create.py` + `views_solicitacao.py`).\n- **PA-04 — Estado inicial**: SUPER começa em `status = 'pendente'`. NAO_SUPER é auto-aprovado na criação (exceção a PA-01), **salvo quando o par município+projeto é sem compra** e quem cria não é isento (superusuário ou a capacidade `create_solicitation_without_purchase`): aí nasce `pendente` em qualquer fluxo (`resolve_initial_status(sem_compra=True)`, marca `par_sem_compra` da Regra 4 do `SolicitacaoSerializer`; 03\u002F11\u002F2026). Ver o adendo da PA-04 sobre evento sem compra em `politica-aprovacao.spec.md`. Os importadores não passam `sem_compra`.\n- **PA-05 — Auditoria**: registra usuário, data\u002Fhora e justificativa em `AuditLog` (`APPROVE`\u002F`REJECT`, com `solicitacao_id`, `prev_status`, `new_status`, `justificativa`; não existe model `Aprovacao`). Actions: `APPROVE`, `REJECT`, `PREVIEW_GCAL`, `PUBLISH_GCAL_REQUESTED`, `PUBLISH_GCAL`.\n- **PA-06 — UI\u002FUX**: telas de requester\u002Fcoordenador mostram status e escondem botões sem permissão. `ApprovalsPage.tsx` renderiza Approve\u002FReject só se `status === 'pendente' && canApprove`.\n- **PA-07 — Testes obrigatórios**: os 7 testes nomeados na spec (`v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md` §Testes que cobrem), que é a lista canônica. Rodar `pytest apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py -v`.\n\n## RF (Requisitos Funcionais) — Functional Requirements\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Frequisitos-funcionais.spec.md`.\n\n- **RF01 — Import de dados**: ETL legado (`apps.dat_ingest`) REMOVIDO. Import atual = command `import_export_contract` (dry-run por padrão; `--apply` exige allowlist) + endpoints DRF (`POST \u002Fapi\u002Fcontrole\u002Fimport-acoes\u002F`, `\u002Fapi\u002Fdat\u002Fimport-cadastros\u002F`). Idempotência via `external_hash` (SHA1\u002FSHA256). Spec: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fimports.spec.md`.\n- **RF02 — Solicitação de evento**: página única (`NewSolicitacaoWizard.tsx`, desde 28\u002F10\u002F2026). Cada evento é um cartão (`CartaoEvento`); no cartão, \"+ horário\" acrescenta um horário, e cada horário vira mais um evento do lote, até 10 (`MAX_EVENTOS`). O envio é sempre o lote (`createSolicitacoesEmLote` → `POST \u002Fapi\u002Fsolicitacoes\u002Flote\u002F`, tudo ou nada). O servidor confere conflitos (RF03) e a regra da compra: NAO_SUPER auto-aprova, salvo par sem compra criado por quem não é isento; esse caso e o SUPER ficam pendentes (PA-01..PA-07).\n- **RF03 — Verificação de conflitos**: `availability_service.py:check_conflicts()` retorna `AvailabilityResult` com lista de `ConflictDetail` (regras RD-01..RD-08). Endpoints `GET \u002Fapi\u002Favailability\u002Fcheck\u002F` e `POST \u002Fapi\u002Favailability\u002Fcheck-many\u002F`. Frontend `pages\u002FSolicitacoes\u002FNewSolicitacaoWizard.tsx` com feedback visual por tipo: X\u002FT\u002FP\u002FD bloqueiam (alerta vermelho, botão desabilitado); o M (aviso azul) não é mais emitido desde 05\u002F10\u002F2026.\n- **RF04 — Fluxo de aprovação**: política PA acima. `PATCH \u002Fapi\u002Fsolicitacoes\u002F{id}\u002Fapprove\u002F` (AuditLog APPROVE) e `PATCH ...\u002Freject\u002F` (REJECT). Frontend `ApprovalsPage.tsx`.\n- **RF05 — Google Calendar**: factory `gcal_client_factory.py` (fake vs google). Idempotência `eventId=asv2{id}` (sem hífen: o Google só aceita base32hex) + `gcal_payload_hash` (SHA1 do payload sem `conferenceData`; não trocar o algoritmo: invalida os hashes gravados). Retry 3x (1s\u002F2s\u002F4s) para 429\u002F5xx. Preview (`\u002Fpreview-gcal\u002F`) gera payload sem persistir; Publish (`\u002Fpublish\u002F`) enfileira Celery e retorna 202. Vars: `GCAL_CLIENT` (fake|google), `GCAL_AUTH_MODE` (prod = `oauth`), `GCAL_OAUTH_CALENDAR_ID` (calendário da organização: publicação e cópia Google→sistema), `GCAL_SEND_UPDATES`. `GCAL_CALENDAR_ID` e `GOOGLE_SERVICE_ACCOUNT_FILE`\u002F`_JSON` só valem no modo service account (`preagenda_to_gcal`, auto-apply). Contrato: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fgcal.spec.md`.\n- **RF06 — Google Meet**: campo `meet_link` (read-only no serializer). `is_online=false` (default) → presencial, sem `conferenceData`; `is_online=true` → online com `conferenceData`, gera link. Persiste o `hangoutLink` SÓ no apply real (não em preview\u002Fdry_run\u002F409). Componente `MeetLink.tsx`.\n- **RF07 — Auditoria**: model `AuditLog` (usuario, action, model_name, details JSON, created_at). Actions: APPROVE, REJECT, PREVIEW_GCAL, PUBLISH_GCAL_REQUESTED, PUBLISH_GCAL, RESYNC_GCAL_REQUESTED, CANCEL_GCAL_REQUESTED, CANCEL_GCAL.\n- **RF08 — Mapa mensal**: página `\u002Fdisponibilidade`, 2 grids (Formadores \u002F Coordenadores), filtros ano\u002Fmês\u002Fsetor\u002Fq, linhas virtualizadas, export CSV, cache Redis 5 min. Códigos por célula: E (1 evento), 2 (2+), P\u002FT (bloqueios), X (evento+bloqueio), D\u002FD1 (deslocamento). API `GET \u002Fapi\u002Favailability\u002Fmonthly\u002F?year=&month=&role=&sector=&q=`.\n\n## Módulos de domínio (REFERENCE)\n\nFatos consultados sob demanda (modelos, campos, workflows) — abrir só quando precisar:\n\n- **DAT** (ações, registros, cadastros, coordenadores): `reference\u002Fdat-module.md`\n- **PlanoFormacoes** (planos anuais, formações, acompanhamentos, provas): `reference\u002Fplano-formacoes.md`\n- **Compras** (DATCompra, Produto; a `Compra` antiga foi apagada no G29): `reference\u002Fcompras.md`\n- **Key Models** (Usuario, Municipio, Projeto, Solicitacao, AvailabilityBlock, AuditLog): `reference\u002Fkey-models.md`\n\n## Related Skills\n\n- **`django-patterns`** — padrões de implementação (models, views, serializers).\n- **`create-plan`** \u002F **`implement-plan`** — arquitetura e planejamento.\n- Imports\u002Fdata-loading: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fimports.spec.md` (skill `etl-guidelines` DEPRECADA — ETL legado removido).\n\n## Quick Reference Commands\n\n```bash\n# RD-01..RD-08 (conflitos)\npytest apps\u002Fcore\u002Ftests\u002Ftest_availability_service.py -v\n\n# PA-01..PA-07 (aprovação)\npytest apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py -v\n\n# Google Calendar\npytest apps\u002Fcore\u002Ftests\u002Ftest_gcal*.py -v\n\n# Import export-contract (dry-run por padrão; --apply exige allowlist)\ndocker compose exec web python manage.py import_export_contract --path \u003Cdir_do_export>\n\n# Imports via API DRF (dry-run)\nmake import-acoes-dry FILE=...\nmake import-acoes-dry FILE=...\nmake import-cadastros-dry FILE=...\n```\n","# Aprender Sistema v2 — Business Domain\n\nReference for the stable domain contracts (RF \u002F RD \u002F PA \u002F CP) and core models.\n\n> **SSOT dos contratos vivos**: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002F*.spec.md` (modelo SDD, ADR-017).\n> Cada spec tem `status` + `last_verified` + `sources_of_truth`. Não duplicar métricas\n> datadas aqui; este skill resume a regra e aponta para a spec viva.\n\n## CP (Cláusulas Pétreas) — IMMUTABLE\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fclausulas-petreas.spec.md` (CP-01..CP-08).\n\n- **CP-01**: v2 roda APENAS em Docker (`cd v2 && make up`). Validação `REQUIRE_DOCKER` em `config\u002Fsettings.py`.\n- **CP-02**: PA-01..PA-07 — aprovação manual obrigatória (ver PA abaixo).\n- **CP-03**: RD-01..RD-08 — disponibilidade com timezone Fortaleza (ver RD abaixo).\n- **CP-04**: Workflow Entender → Planejar → Implementar → Testar. Nunca pular etapas.\n- **CP-05** a **CP-08**: v1 congelado; conventional commits; nunca push direto na main (hook); `INCLUDE_DEV_TOOLS=false` em produção.\n\n## RD (Regras de Disponibilidade) — Availability Rules\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fregras-disponibilidade.spec.md`.\nImplementação: `apps\u002Fcore\u002Fservices\u002Favailability_service.py:check_conflicts()`.\nTestes: `apps\u002Fcore\u002Ftests\u002Ftest_availability_service.py`.\n\n- **RD-01 — Non-overlapping**: quem ocupa a agenda (`ENFORCED_ROLES`: FORMADOR e, desde 03\u002F11\u002F2026, COORD_ACOMPANHA; o coordenador responsável só com `coordenador_acompanha=True`; ver `ocupa_agenda_q`) não pode ter dois eventos que se sobreponham (≥1 min). `end == start` é adjacente, NÃO é conflito.\n- **RD-02 — Bloqueio total (T)**: bloqueio marcado `T` impede QUALQUER evento no intervalo. Código `T`.\n- **RD-03 — Bloqueio parcial (P)**: bloqueio `P` impede eventos só dentro do subintervalo; fora é permitido.\n- **RD-04 — Deslocamento (D)**: entre municípios diferentes exige tempo mínimo de viagem (configurável). Mesmo município pode ser zero.\n- **RD-05 — Teto de horas por evento (contagem de CH)**: desde 05\u002F10\u002F2026 (decisão do dono, OK explícito para mudar RD-05\u002FCP-03) **não é checagem de agenda**: o motor não emite `M` em nenhum caminho (de 02\u002F10 a 05\u002F10 foi aviso; antes barrava). O parâmetro `AVAILABILITY_DAILY_LIMIT_HOURS` (Configurações, \"Teto de horas por evento (contagem)\", 1 a 12, padrão 8) é o teto da contagem de horas de formação: CH de um evento = `min(fim − início, teto)`; as CH do dia somam (sem teto por dia); evento que cruza a meia-noite conta no dia local do início; sem desconto de almoço; online conta; só aprovado, qualquer projeto; conta sempre quem ocupa a agenda pelo papel (`ENFORCED_ROLES` = FORMADOR e, desde 03\u002F11\u002F2026, COORD_ACOMPANHA, os coordenadores além do responsável); o coordenador responsável (`coordenador`; sem ele, quem criou) só com `coordenador_acompanha=True`; convidado não conta; a mesma pessoa conta uma vez por evento. SSOT `services\u002Fhoras_formacao.py` (`ocupacoes_aprovadas`: Grade Mensal `ch_month`\u002F`ch_year`, painel de Equipe `horas_trabalhadas` e a população do ranking de formadores).\n- **RD-06 — Timezone**: comparações timezone-aware em `America\u002FFortaleza`; armazenamento em UTC.\n- **RD-07 — Ordem dos checks**: (1) blocos T\u002FP → (2) conflitos de eventos aprovados → (3) deslocamento D. Todos barram (`conflicts`); `warnings` só existe na checagem prévia (`check\u002F`, `check-many\u002F`) e vem vazio desde 05\u002F10\u002F2026 (saiu do guard e do 400 em 06\u002F10\u002F2026).\n- **RD-08 — Mensagens de conflito**: cada `Conflict` carrega `code`, `title`, `detail` (formador e intervalo `HH:MM dd\u002Fmm`) e `ref_id` opcional. Os códigos emitidos são `X`, `T`, `P` e `D`; não há mensagem de limite diário (o RD-05 é contagem de horas, não conflito).\n\n```json\n{ \"code\": \"T\", \"title\": \"Bloqueio total\", \"detail\": \"Maria Silva - 15\u002F01\u002F2025 09:00-12:00\", \"ref_id\": 123 }\n```\n\n## PA (Política de Aprovação) — Approval Policy\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md`.\nTestes: `apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py`.\n\n- **PA-01 — No auto-approval (SUPER)**: `Solicitacao` com `projeto.fluxo == 'SUPER'` NUNCA vira \"aprovado\" automaticamente, mesmo sem conflito. Decidido por `resolve_initial_status()` (`apps\u002Fcore\u002Fservices\u002Fsolicitacao_create.py:23`), chamado no `perform_create` do `SolicitacaoViewSet` (`views_solicitacao.py`) — **não** há override de `Solicitacao.save()`.\n- **PA-02 — Perfil exigido**: regra única `solicitation_approval_basis`; ver PA-02 em `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md` (nenhum grupo aprova). Idioma canônico `permission_classes = [HasPerm(\"codename\")]` (`from apps.core.rbac import HasPerm`; políticas em `apps\u002Fcore\u002Frbac\u002F`).\n- **PA-03 — Triggers pós-aprovação**: integrações externas (GCal, RF05\u002FRF06) só executam APÓS a aprovação manual; a task de publicação não é chamada na criação (fluxo service-layer: `solicitacao_create.py` + `views_solicitacao.py`).\n- **PA-04 — Estado inicial**: SUPER começa em `status = 'pendente'`. NAO_SUPER é auto-aprovado na criação (exceção a PA-01), **salvo quando o par município+projeto é sem compra** e quem cria não é isento (superusuário ou a capacidade `create_solicitation_without_purchase`): aí nasce `pendente` em qualquer fluxo (`resolve_initial_status(sem_compra=True)`, marca `par_sem_compra` da Regra 4 do `SolicitacaoSerializer`; 03\u002F11\u002F2026). Ver o adendo da PA-04 sobre evento sem compra em `politica-aprovacao.spec.md`. Os importadores não passam `sem_compra`.\n- **PA-05 — Auditoria**: registra usuário, data\u002Fhora e justificativa em `AuditLog` (`APPROVE`\u002F`REJECT`, com `solicitacao_id`, `prev_status`, `new_status`, `justificativa`; não existe model `Aprovacao`). Actions: `APPROVE`, `REJECT`, `PREVIEW_GCAL`, `PUBLISH_GCAL_REQUESTED`, `PUBLISH_GCAL`.\n- **PA-06 — UI\u002FUX**: telas de requester\u002Fcoordenador mostram status e escondem botões sem permissão. `ApprovalsPage.tsx` renderiza Approve\u002FReject só se `status === 'pendente' && canApprove`.\n- **PA-07 — Testes obrigatórios**: os 7 testes nomeados na spec (`v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Fpolitica-aprovacao.spec.md` §Testes que cobrem), que é a lista canônica. Rodar `pytest apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py -v`.\n\n## RF (Requisitos Funcionais) — Functional Requirements\n\nSSOT: `v2\u002Fdocs\u002Fspecs\u002Fdomain\u002Frequisitos-funcionais.spec.md`.\n\n- **RF01 — Import de dados**: ETL legado (`apps.dat_ingest`) REMOVIDO. Import atual = command `import_export_contract` (dry-run por padrão; `--apply` exige allowlist) + endpoints DRF (`POST \u002Fapi\u002Fcontrole\u002Fimport-acoes\u002F`, `\u002Fapi\u002Fdat\u002Fimport-cadastros\u002F`). Idempotência via `external_hash` (SHA1\u002FSHA256). Spec: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fimports.spec.md`.\n- **RF02 — Solicitação de evento**: página única (`NewSolicitacaoWizard.tsx`, desde 28\u002F10\u002F2026). Cada evento é um cartão (`CartaoEvento`); no cartão, \"+ horário\" acrescenta um horário, e cada horário vira mais um evento do lote, até 10 (`MAX_EVENTOS`). O envio é sempre o lote (`createSolicitacoesEmLote` → `POST \u002Fapi\u002Fsolicitacoes\u002Flote\u002F`, tudo ou nada). O servidor confere conflitos (RF03) e a regra da compra: NAO_SUPER auto-aprova, salvo par sem compra criado por quem não é isento; esse caso e o SUPER ficam pendentes (PA-01..PA-07).\n- **RF03 — Verificação de conflitos**: `availability_service.py:check_conflicts()` retorna `AvailabilityResult` com lista de `ConflictDetail` (regras RD-01..RD-08). Endpoints `GET \u002Fapi\u002Favailability\u002Fcheck\u002F` e `POST \u002Fapi\u002Favailability\u002Fcheck-many\u002F`. Frontend `pages\u002FSolicitacoes\u002FNewSolicitacaoWizard.tsx` com feedback visual por tipo: X\u002FT\u002FP\u002FD bloqueiam (alerta vermelho, botão desabilitado); o M (aviso azul) não é mais emitido desde 05\u002F10\u002F2026.\n- **RF04 — Fluxo de aprovação**: política PA acima. `PATCH \u002Fapi\u002Fsolicitacoes\u002F{id}\u002Fapprove\u002F` (AuditLog APPROVE) e `PATCH ...\u002Freject\u002F` (REJECT). Frontend `ApprovalsPage.tsx`.\n- **RF05 — Google Calendar**: factory `gcal_client_factory.py` (fake vs google). Idempotência `eventId=asv2{id}` (sem hífen: o Google só aceita base32hex) + `gcal_payload_hash` (SHA1 do payload sem `conferenceData`; não trocar o algoritmo: invalida os hashes gravados). Retry 3x (1s\u002F2s\u002F4s) para 429\u002F5xx. Preview (`\u002Fpreview-gcal\u002F`) gera payload sem persistir; Publish (`\u002Fpublish\u002F`) enfileira Celery e retorna 202. Vars: `GCAL_CLIENT` (fake|google), `GCAL_AUTH_MODE` (prod = `oauth`), `GCAL_OAUTH_CALENDAR_ID` (calendário da organização: publicação e cópia Google→sistema), `GCAL_SEND_UPDATES`. `GCAL_CALENDAR_ID` e `GOOGLE_SERVICE_ACCOUNT_FILE`\u002F`_JSON` só valem no modo service account (`preagenda_to_gcal`, auto-apply). Contrato: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fgcal.spec.md`.\n- **RF06 — Google Meet**: campo `meet_link` (read-only no serializer). `is_online=false` (default) → presencial, sem `conferenceData`; `is_online=true` → online com `conferenceData`, gera link. Persiste o `hangoutLink` SÓ no apply real (não em preview\u002Fdry_run\u002F409). Componente `MeetLink.tsx`.\n- **RF07 — Auditoria**: model `AuditLog` (usuario, action, model_name, details JSON, created_at). Actions: APPROVE, REJECT, PREVIEW_GCAL, PUBLISH_GCAL_REQUESTED, PUBLISH_GCAL, RESYNC_GCAL_REQUESTED, CANCEL_GCAL_REQUESTED, CANCEL_GCAL.\n- **RF08 — Mapa mensal**: página `\u002Fdisponibilidade`, 2 grids (Formadores \u002F Coordenadores), filtros ano\u002Fmês\u002Fsetor\u002Fq, linhas virtualizadas, export CSV, cache Redis 5 min. Códigos por célula: E (1 evento), 2 (2+), P\u002FT (bloqueios), X (evento+bloqueio), D\u002FD1 (deslocamento). API `GET \u002Fapi\u002Favailability\u002Fmonthly\u002F?year=&month=&role=&sector=&q=`.\n\n## Módulos de domínio (REFERENCE)\n\nFatos consultados sob demanda (modelos, campos, workflows) — abrir só quando precisar:\n\n- **DAT** (ações, registros, cadastros, coordenadores): `reference\u002Fdat-module.md`\n- **PlanoFormacoes** (planos anuais, formações, acompanhamentos, provas): `reference\u002Fplano-formacoes.md`\n- **Compras** (DATCompra, Produto; a `Compra` antiga foi apagada no G29): `reference\u002Fcompras.md`\n- **Key Models** (Usuario, Municipio, Projeto, Solicitacao, AvailabilityBlock, AuditLog): `reference\u002Fkey-models.md`\n\n## Related Skills\n\n- **`django-patterns`** — padrões de implementação (models, views, serializers).\n- **`create-plan`** \u002F **`implement-plan`** — arquitetura e planejamento.\n- Imports\u002Fdata-loading: `v2\u002Fdocs\u002Fspecs\u002Fbackend\u002Fimports.spec.md` (skill `etl-guidelines` DEPRECADA — ETL legado removido).\n\n## Quick Reference Commands\n\n```bash\n# RD-01..RD-08 (conflitos)\npytest apps\u002Fcore\u002Ftests\u002Ftest_availability_service.py -v\n\n# PA-01..PA-07 (aprovação)\npytest apps\u002Fcore\u002Ftests\u002Ftest_approval_policy_PA.py -v\n\n# Google Calendar\npytest apps\u002Fcore\u002Ftests\u002Ftest_gcal*.py -v\n\n# Import export-contract (dry-run por padrão; --apply exige allowlist)\ndocker compose exec web python manage.py import_export_contract --path \u003Cdir_do_export>\n\n# Imports via API DRF (dry-run)\nmake import-acoes-dry FILE=...\nmake import-acoes-dry FILE=...\nmake import-cadastros-dry FILE=...\n```\n",{"name":5,"description":7},"main","9abb885bbe6d35c3637e3755ece6a7d2216ee454",".claude\u002Fskills\u002Faprender-domain","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa","Dev Python | IA | Automation | HTML | CSS | Data | Process | Projects","2026-11-11 08:34:26",{"source":8,"skillName":5,"commands":52,"prompt":62,"zipAvailable":63},[53,56,59],{"tool":54,"command":55},"npx","npx skills add matheusnorjosa\u002Faprender_sistema --skill aprender-domain",{"tool":57,"command":58},"bunx","bunx skills add matheusnorjosa\u002Faprender_sistema --skill aprender-domain",{"tool":60,"command":61},"pnpm","pnpm dlx skills add matheusnorjosa\u002Faprender_sistema --skill aprender-domain","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002FSKILL.md",true,[65,71,75,79],{"path":66,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":69,"sourceHtmlUrl":70},"reference\u002Fcompras.md","REFERENCE","参考文档",1618,"https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002Freference\u002Fcompras.md",{"path":72,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":73,"sourceHtmlUrl":74},"reference\u002Fdat-module.md",4063,"https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002Freference\u002Fdat-module.md",{"path":76,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":77,"sourceHtmlUrl":78},"reference\u002Fkey-models.md",3062,"https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002Freference\u002Fkey-models.md",{"path":80,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":81,"sourceHtmlUrl":82},"reference\u002Fplano-formacoes.md",3246,"https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Faprender-domain\u002Freference\u002Fplano-formacoes.md",[84,121,142,163,175,206],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":100,"publishedAt":39},"01M6C0WHSP7S2QMJGGMQRXJ6PD","writing-standards","Apply AS v2 technical-writing standards (clarity, brevity, active voice, Python\u002FDjango conventions). Use when writing a README, a docstring (PEP 257), a commit message, a PR description, an error message, or any technical doc.","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fwriting-standards\u002FSKILL.md",[90,95],{"categoryNo":91,"categoryName":92,"taxonomyType":93,"taxonomyTypeDesc":94},"01JBSEEDCAT000000000000018","写作与文案","SCENARIO","使用场景",{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[36,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,22],"writing standards","standards","python","commit","pr","pr description","commit message","doc","voice","clarity","django","technical","README","message","active","as","error","technical-writing","brevity","docstring",{"skillNo":122,"name":123,"nameZh":6,"description":124,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":127,"publishedAt":39},"01M6C0WG2SBP9PJG1YQFGGVJAR","verification-gate","Verify before claiming success. Use when about to say something works\u002Fis done, after implementing a feature, after a bugfix, or before pushing — run the proof command and confirm output first.","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fverification-gate\u002FSKILL.md",[],[128,129,130,131,132,133,134,28,135,136,137,21,138,139,140,141,29,22],"verification","verification gate","gate","run","first","verify","command","verify before claiming","say","done","proof","bugfix","output","about",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":151,"publishedAt":39},"01M6C0WE010G12KH45YZW6M7K5","test-driven-development","Write the failing test before the implementation in AS v2 (pytest + Django\u002FDRF). Use when implementing a feature or fixing a bug, before any production code is written.","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ftest-driven-development\u002FSKILL.md",[148],{"categoryNo":149,"categoryName":150,"taxonomyType":93,"taxonomyTypeDesc":94},"01JBSEEDCAT000000000000008","测试与质量",[38,152,153,154,155,156,111,157,28,158,159,160,21,116,161,29,22,162],"test driven development","development","driven","code","Production","write","bug","implementation","pytest","drf","failing",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":39},"01M6C0WC6Y81XRVBJQSTKA96XQ","subagent-development","Orquestra subagents no AS v2 — decompõe a tarefa em domínios independentes, paraleliza e revisa em 2 estágios. Use ao delegar exploração, implementação ou validação cross-arquivo para acelerar o trabalho e poupar o contexto principal.","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fsubagent-development\u002FSKILL.md",[],[153,170,171,172,173,28,174,116],"subagent","subagent development","para","subagents","ao",{"skillNo":176,"name":177,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":181,"publishedAt":39},"01M6C0W9W8D6JJWXW28Q7ZHAC9","security-scan","Automated security scanning for AS v2. Use before commits or when reviewing changes for vulnerabilities. Runs dependency audit (pip-audit, npm audit), secret detection (grep patterns), and code pattern scanning (SQL injection, XSS, eval, pickle, hardcoded secrets). Integrates with project hooks system.","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fsecurity-scan\u002FSKILL.md",[],[182,183,184,185,186,155,187,188,189,190,191,192,193,194,195,196,197,198,28,199,200,201,116,202,203,204,30,205,22],"security","security scan","scan","vulnerabilities","sql","npm","audit","dependency audit","detection","injection","xss","secrets","pip-audit","commits","system","grep","eval","hooks","security scanning","project","with","true","pip","changes",{"skillNo":207,"name":208,"nameZh":6,"description":209,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":210,"categories":211,"keywords":215,"publishedAt":39},"01M6C0W7RE3G3C2852YXQKR5J9","resume-handoff","Resume work from a saved handoff document, validating its state against the current codebase before continuing. Use when resuming work from a saved handoff after a context clear or in a new session (path, session name, or GitHub issue).","https:\u002F\u002Fgithub.com\u002Fmatheusnorjosa\u002Faprender_sistema\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fresume-handoff\u002FSKILL.md",[212],{"categoryNo":213,"categoryName":214,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000038","人力资源",[216,217,218,219,220,221,222,223,224,28,225,226,227,228,229,230,231,232,22,233],"handoff","resume handoff","resume","github","state","new session","work","context","session","github issue","issue","document","clear","name","saved","codebase","resuming","new"]