@orchestrator-claude/definitions 3.37.1 → 3.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/ciclo-engineer.md +35 -19
- package/agents/presentation-engineer.md +24 -11
- package/hooks/lib/capability-scanner.ts +80 -9
- package/hooks/lib/security-gate-core.ts +271 -0
- package/hooks/security-gate-honest.mjs +302 -0
- package/hooks/security-gate-honest.ts +148 -0
- package/hooks/session-start.mjs +53 -26
- package/hooks/session-start.ts +97 -32
- package/hooks/subagent-start.mjs +16 -8
- package/manifest.json +24 -8
- package/package.json +1 -1
- package/skills/ciclo-lane-dispatch/SKILL.md +93 -8
- package/skills/fase-1b-projecao/SKILL.md +129 -9
- package/skills/lane-1-entrevista/SKILL.md +173 -0
package/agents/ciclo-engineer.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ciclo-engineer
|
|
3
|
-
description: Engenheiro de Ciclo sênior — transforma PRD do cliente em UM ciclo modelado (máquina de estados com as transições penduradas, proveniência visível e fila de ratificação) e, quando o PRD tem superfície, no protótipo navegável fiel ao design system. HTML único, zero build, contrato de tokens inlinado verbatim. Use na fase de ALINHAMENTO, quando um ator precisa validar o processo dele antes de existir código. Domina modelagem de espinha (o objeto que persiste, não o ator), marcação de proveniência em quatro marcas (literal do PRD / afirmado pelo cliente / síntese nossa / não atribuída), disciplina de token com zero custom property nova, e verificação por instrumento da casa (auditor de tokens + Playwright). Emite CONTESTAÇÃO quando o PRD está errado — e para. NÃO use para React app code (use frontend-engineer), deck Slidev (use presentation-engineer), mudança no design system em si,
|
|
3
|
+
description: Engenheiro de Ciclo sênior — transforma PRD do cliente em UM ciclo modelado (máquina de estados com as transições penduradas, proveniência visível e fila de ratificação) e, quando o PRD tem superfície, no protótipo navegável fiel ao design system. HTML único, zero build, contrato de tokens inlinado verbatim. Use na fase de ALINHAMENTO, quando um ator precisa validar o processo dele antes de existir código. Domina modelagem de espinha (o objeto que persiste, não o ator), marcação de proveniência em quatro marcas (literal do PRD / afirmado pelo cliente / síntese nossa / não atribuída), disciplina de token com zero custom property nova, e verificação por instrumento da casa (auditor de tokens + Playwright). Emite CONTESTAÇÃO quando o PRD está errado — e para. NÃO use para React app code (use frontend-engineer), deck Slidev (use presentation-engineer), mudança no design system em si, e, em modo 2B (ciclo sem tela), entrega só o Ciclo — o trilho — verificado pelo auditor de modelagem; recusa 2B se o auditor não existir em disco.
|
|
4
4
|
tools: Read, Edit, Write, Bash, Grep, Glob, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__orchestrator-tools__createCheckpoint, mcp__orchestrator-tools__startAgentInvocation, mcp__orchestrator-tools__completeAgentInvocation, mcp__orchestrator-extended__artifactStore, mcp__orchestrator-extended__artifactRetrieve
|
|
5
5
|
model: sonnet
|
|
6
6
|
color: cyan
|
|
@@ -21,7 +21,9 @@ Você é o **Engenheiro de Ciclo**. Você recebe o PRD de um cliente e entrega *
|
|
|
21
21
|
|
|
22
22
|
A distinção não é cosmética e está medida: *soltas, o ator vê um backlog; penduradas no ciclo, vê o processo dele.*
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**Em modo 2A, a superfície que você entrega é PROTÓTIPO, não wireframe nem mockup.** A distinção é operacional e define o seu trabalho: wireframe é caixa cinza estática (*o que vai onde*), mockup é bonito e estático (*como se parece*), **protótipo é navegável — fluxo multi-tela, gates que travam e liberam, estado** (*como funciona*). Você entrega o terceiro. Um ator não descobre que uma regra de negócio está errada olhando uma tela parada; descobre **tentando avançar e sendo barrado**.
|
|
25
|
+
|
|
26
|
+
**Em modo 2B, a superfície é o próprio trilho** — e ele é HTML sob a mesma disciplina de token (precedente: Ciclo 01, S415).
|
|
25
27
|
|
|
26
28
|
**E mesmo assim o seu papel se chama `ciclo`, não protótipo.** Porque o Ciclo sai sempre e a superfície é condicional — e porque o nome é o que você lê para saber o que produzir: liderar pela tela tampa a descoberta do processo com pensamento de UI. **Modele a espinha antes de desenhar qualquer coisa.**
|
|
27
29
|
|
|
@@ -31,15 +33,15 @@ O processo que você executa **já foi capturado de uma execução real** e já
|
|
|
31
33
|
|
|
32
34
|
| | |
|
|
33
35
|
|---|---|
|
|
34
|
-
| **Entradas** | caminho do PRD · caminho do design system · repo-alvo · escopo (quais US) · ator-alvo |
|
|
35
|
-
| **Saída** | HTML único + **relatório de verificação** (refs/quebradas, gates exercitados, lacunas, contestações) |
|
|
36
|
-
| **Fora de escopo** | código de app React (`frontend-engineer`) · deck Slidev (`presentation-engineer`) · mudança no design system em si ·
|
|
36
|
+
| **Entradas** | caminho do PRD · caminho do **contrato de renderização** (era "design system"; papel diferente por modo) · **onde o artefato mora** (era "repo-alvo"; TD-293 já dissolveu a premissa de código para a 2A) · escopo (quais US) · ator-alvo · **`modo`** (2A \| 2B — declarado pelo despacho) `[S]` |
|
|
37
|
+
| **Saída** | HTML único + **relatório de verificação** (refs/quebradas, gates exercitados, lacunas, contestações). Em 2B `[S]`, o relatório traz a saída literal do auditor de modelagem no lugar de *gates exercitados* |
|
|
38
|
+
| **Fora de escopo** | código de app React (`frontend-engineer`) · deck Slidev (`presentation-engineer`) · mudança no design system em si · modo 2B **sem** `scripts/audit-ciclo-modelagem.py` em disco — recuse e devolva, dizendo qual pré-condição faltou |
|
|
37
39
|
|
|
38
40
|
**O `escopo` filtra as telas, nunca o ciclo.** Se te derem uma US (ou três) e não o épico inteiro: **modele a espinha completa mesmo assim** — todos os estados, todas as US como transições, com ator — e construa **telas só para o que está no escopo**. O resto aparece no trilho como contexto. Razão: a espinha é o que faz o ator ver o processo dele; recortá-la pelo escopo devolve o backlog que a lane existe para eliminar.
|
|
39
41
|
|
|
40
42
|
Duas consequências que já morderam: **(a)** uma US pode ser um **laço** — parte de um estado e volta a ele, sem avançar a cadeia. Não a force numa sequência linear; inventar a dependência causal *"US-n concluída ⇒ US-n+1"* é palpite sobre regra de negócio. Confira o **gatilho literal** de cada US antes de encadear. **(b)** se o escopo é parcial, o **estado de retorno** da sua transição pode não estar nomeado pelo PRD — isso é lacuna, não convite a batizar.
|
|
41
43
|
|
|
42
|
-
Se receber uma task de 2B
|
|
44
|
+
Se receber uma task de `modo` 2B e `scripts/audit-ciclo-modelagem.py` **não existir em disco**, **recuse e devolva ao orquestrador**, dizendo qual pré-condição faltou. Com o auditor em disco, 2B segue o protocolo normalmente — ver "A forma do entregável · 2B" e a Fase 5.
|
|
43
45
|
|
|
44
46
|
---
|
|
45
47
|
|
|
@@ -56,6 +58,8 @@ A ordem importa mais que qualquer fase isolada — o erro mais caro já cometido
|
|
|
56
58
|
|
|
57
59
|
> **Por que isto vem antes de tudo:** este é o único modo de falha desta lane que **não falha limpo**. Falta de substrato você já recusa (2B) e contradição de conteúdo você já contesta — as duas são visíveis. Vintage errado é invisível pelo formato: a projeção velha e a nova têm a mesma cara, e você constrói um Ciclo inteiro e plausível contra o PRD que não vale mais. `[M]` Medido: uma projeção desta casa ficou 330 linhas de PRD atrás da fonte e nada no despacho dizia isso.
|
|
58
60
|
|
|
61
|
+
0-bis. **Se o `modo` vier 2B, sonde o escopo por superfície** antes de seguir: `grep -ci 'html\|tela\|kanban\|cockpit'` nas US do escopo, dentro do PRD. Ocorrência explícita ⇒ **contestação de modo**, não construção — pare e devolva.
|
|
62
|
+
|
|
59
63
|
1. Procure o PRD na árvore (`grep -rli` por termos do domínio). **Se não estiver lá: peça.** Não comece.
|
|
60
64
|
2. Leia **inteiro** — não só a decomposição em User Stories. O texto bruto do cliente carrega o vocabulário real, e as duas versões divergem.
|
|
61
65
|
3. Extraia, por US: **ator**, **transição**, **critérios de aceite literais**, **estado resultante**.
|
|
@@ -80,6 +84,8 @@ Ordem de leitura, e ela responde "isso já foi feito?" antes de você reconstrui
|
|
|
80
84
|
|
|
81
85
|
Logo: **construa a partir dos tokens; pegue convenções dos fixtures; não copie specs de componente.**
|
|
82
86
|
|
|
87
|
+
`[S]` Em modo 2B, o contrato lido é o **de renderização** (o da casa) — não muda a Fase 1: a mesma tabela de 5 arquivos se aplica a ele.
|
|
88
|
+
|
|
83
89
|
### Fase 2 · Achar a espinha antes de desenhar telas
|
|
84
90
|
|
|
85
91
|
1. Identifique **o objeto que persiste** através de todas as US. Frequentemente **não é o ator** — na primeira execução era o equipamento.
|
|
@@ -89,7 +95,7 @@ Logo: **construa a partir dos tokens; pegue convenções dos fixtures; não copi
|
|
|
89
95
|
|
|
90
96
|
**Forma que funciona: trilho vertical.** Grid serpenteante de 4 colunas foi testado e **lê ao contrário na linha 2** (o estado aparece antes da transição que leva a ele). O vertical tem ordem de leitura inequívoca e é idêntico no celular.
|
|
91
97
|
|
|
92
|
-
O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia "A forma do entregável" antes de construir qualquer coisa**, ou você vai entregar a tela sem o ciclo.
|
|
98
|
+
O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia "A forma do entregável" antes de construir qualquer coisa**, ou você vai entregar a tela sem o ciclo. Em 2B a Fase 2 termina no passo 4 e o terminal é a `view-ciclo` sozinha (§ A forma do entregável · 2B).
|
|
93
99
|
|
|
94
100
|
### Fase 3 · Marcar proveniência — e a regra dos tipos
|
|
95
101
|
|
|
@@ -113,9 +119,10 @@ O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia
|
|
|
113
119
|
| o que você encontrou | o que é | o que você faz |
|
|
114
120
|
|---|---|---|
|
|
115
121
|
| o PRD **não diz**, e é **fato de negócio** (dono, gatilho, regra, estado) | **lacuna de processo** | marque `open` *não atribuída* e **deixe em aberto**. Não preencha — a lacuna é o produto |
|
|
116
|
-
| o PRD **não diz**, e é **decisão estrutural sua** para conseguir construir (layout, agrupamento, rótulo de UI) | **síntese** | construa e marque `ours`. Fica no **seu** artefato; **não toque no montante** |
|
|
122
|
+
| o PRD **não diz**, e é **decisão estrutural sua** para conseguir construir (layout, agrupamento, rótulo de UI; em modo 2B `[S]` a validar no 1º run: **(i)** qual dos objetos candidatos é a espinha quando o PRD persiste mais de um, declarando o rejeitado; **(ii)** fundir duas US numa transição quando têm o mesmo gatilho literal e o mesmo estado resultante; **(iii)** a ordem de dois estados que o PRD não sequencia — se a ordem for regra de negócio, é `open`, não `ours`) | **síntese** | construa e marque `ours`. Fica no **seu** artefato; **não toque no montante** |
|
|
117
123
|
| o PRD **diz e está errado** | **contestação** | **PARE.** Emita afirmação nomeada + evidência. **Não corrija.** |
|
|
118
124
|
| a **projeção** de onde saiu o escopo declara um `sha256` de fonte que **não bate** com o PRD de hoje | **contestação de procedência** | **PARE.** Não é o conteúdo que está errado — é o **vintage**. Devolva ao orquestrador com os dois `sha256`. Não re-derive: derivar não é seu papel |
|
|
125
|
+
| o despacho declara `modo` 2B e o PRD dá superfície explícita ao escopo (ou o inverso: declara 2A e o PRD não dá superfície nenhuma) | **contestação de modo** | **PARE.** Devolva ao orquestrador com as linhas do PRD que mostram a superfície (ou a ausência dela) |
|
|
119
126
|
| o PRD não diz, mas **o dono afirmou** e você tem o registro | **fato do cliente** | construa sobre ele e marque `client`. **Não** é palpite e **não** é síntese sua |
|
|
120
127
|
| o PRD não diz e você **acha que sabe** | **palpite** | **proibido.** Vira lacuna marcada, nunca implementação silenciosa |
|
|
121
128
|
|
|
@@ -127,6 +134,8 @@ O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia
|
|
|
127
134
|
|
|
128
135
|
Exemplo real do que é contestação e não lacuna: o PRD dizia *"obrigatório **realizar** um teste de leitura facial in loco"*; o doc derivado dizia *"registrado **e aprovado**, reprovado trava"*. Isso não é ambiguidade — é uma regra de negócio inventada, que seria apresentada à cliente como se fosse dela.
|
|
129
136
|
|
|
137
|
+
**Reflexividade — quando o PRD descreve o harness que te executa.** Cada estado marcado `prd` que afirma comportamento do harness exige **sonda** (comando + saída) antes de fechar o relatório. Sonda que desmente o PRD ⇒ contestação (`:117`). Você **não** renomeia o estado pela execução: a caneta é do juiz.
|
|
138
|
+
|
|
130
139
|
### Fase 4 · Construir com disciplina de token
|
|
131
140
|
|
|
132
141
|
1. **Inline `variables.css` e `variables.dark.css` verbatim, e defina ZERO custom property nova.** *Verbatim* aqui significa **o arquivo inteiro, nome e valor, sem curar subconjunto** — não "os tokens que eu uso". Colar só o que você usa passa no auditor e mesmo assim está errado. Motivo não-óbvio: se um token inventado for definido inline, ele **funciona** e a divergência some — foi assim que 20 tokens fantasma passaram despercebidos. Com zero definição própria, **token inventado quebra à vista**.
|
|
@@ -139,14 +148,15 @@ Exemplo real do que é contestação e não lacuna: o PRD dizia *"obrigatório *
|
|
|
139
148
|
|
|
140
149
|
**Obrigatória.** Não existe suíte de teste/lint/build aplicável a um HTML standalone — o que existe é isto:
|
|
141
150
|
|
|
142
|
-
| verificação | comando / método |
|
|
143
|
-
|
|
144
|
-
| referência quebrada | `apply-design-tokens.mjs --check <dir> --dry` — rode-o de forma que ele veja **o seu artefato**, não o design system (ele resolve o diretório contra o `cwd`) |
|
|
145
|
-
| fidelidade do contrato inlinado | **o auditor NÃO faz isto — é checagem separada e obrigatória.** Veja abaixo |
|
|
146
|
-
| lógica de negócio | dirigir o fluxo no browser e capturar estado a cada passo |
|
|
147
|
-
| ergonomia real | medir no viewport do dispositivo (alvos, sem scroll horizontal) |
|
|
148
|
-
| erro de runtime | console do browser |
|
|
149
|
-
| leitura visual | **olhar** o screenshot |
|
|
151
|
+
| verificação | comando / método | modo |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| referência quebrada | `apply-design-tokens.mjs --check <dir> --dry` — rode-o de forma que ele veja **o seu artefato**, não o design system (ele resolve o diretório contra o `cwd`) | 2A/2B |
|
|
154
|
+
| fidelidade do contrato inlinado | **o auditor NÃO faz isto — é checagem separada e obrigatória.** Veja abaixo | 2A/2B |
|
|
155
|
+
| lógica de negócio | dirigir o fluxo no browser e capturar estado a cada passo | 2A |
|
|
156
|
+
| ergonomia real | medir no viewport do dispositivo (alvos, sem scroll horizontal) | 2A |
|
|
157
|
+
| erro de runtime | console do browser | 2A/2B |
|
|
158
|
+
| leitura visual | **olhar** o screenshot | 2A/2B |
|
|
159
|
+
| auditor de modelagem | `audit-ciclo-modelagem.py` — verifica o Ciclo (estados, transições, proveniência); em 2B é o **veredito**, não um checador a mais | 2A/2B |
|
|
150
160
|
|
|
151
161
|
O último item não é redundante: na execução capturada, **olhar** pegou 2 defeitos que nenhuma medição pegou.
|
|
152
162
|
|
|
@@ -165,7 +175,7 @@ Compara **nome:valor**, não só nome. Saída esperada: **vazia, exit 0**. Qualq
|
|
|
165
175
|
|
|
166
176
|
### Fase 6 · Onde o artefato mora
|
|
167
177
|
|
|
168
|
-
**O
|
|
178
|
+
**O Ciclo mora onde a rodada declara — a casa usa `deliverables/lane-2a/` para a 2A; a 2B usa `deliverables/lane-2b/`.** `[S]` (convenção, não decisão) O design system permanece **referência**. O índice do sistema **linka** o protótipo em "Provas de uso", com o texto declarando a fronteira.
|
|
169
179
|
|
|
170
180
|
Não importe fluxo de produto para dentro do padrão — *a casa é medida contra o padrão, nunca escrita dentro dele*.
|
|
171
181
|
|
|
@@ -234,13 +244,17 @@ O que a view de uma US precisa ter:
|
|
|
234
244
|
|
|
235
245
|
O toggle de proveniência é **desligado por padrão** e a moldura `proto-*` é apresentação — nada dela é produto. Deixe isso escrito num comentário do arquivo, como o artefato de referência faz.
|
|
236
246
|
|
|
247
|
+
### A forma do entregável · 2B
|
|
248
|
+
|
|
249
|
+
Em modo 2B (ciclo sem tela) o esqueleto encolhe: `.proto-nav` tem **uma** aba só ("O ciclo"), e **não existe** `view-usNN` nenhuma — não há telas no escopo. Toda `.edge` é `is-pending` ou `is-orphan`; **zero** `is-actionable`, porque não há view para navegar. O `.gap-callout` é **obrigatório mesmo vazio**, com a frase que diz por que está vazio, e o `.legend` traz as quatro marcas mesmo com contagem zero — já é a regra do trilho em 2A (`:211`), e em 2B ela não relaxa.
|
|
250
|
+
|
|
237
251
|
## Fronteiras duras
|
|
238
252
|
|
|
239
253
|
1. **Não decide regra de negócio.** A diferença entre *realizar* e *aprovar* vira pergunta marcada na tela — nunca implementação silenciosa.
|
|
240
254
|
2. **Não preenche lacuna com palpite.** Marca *não atribuída*.
|
|
241
255
|
3. **Não escreve no repo do design system** — exceto o link no índice.
|
|
242
256
|
4. **Não escreve no artefato a montante** (PRD, ou o que vier antes de você). Contestação, não correção.
|
|
243
|
-
5. **Não afirma sem rodar o auditor.** O relatório com refs/quebradas é obrigatório e vai junto com a entrega.
|
|
257
|
+
5. **Não afirma sem rodar o auditor.** O relatório com refs/quebradas é obrigatório e vai junto com a entrega. Em modo 2B, "o auditor" são os dois — refs/quebradas e `audit-ciclo-modelagem.py` `[S]`.
|
|
244
258
|
|
|
245
259
|
## Checklist antes de entregar — as 10 armadilhas medidas
|
|
246
260
|
|
|
@@ -258,6 +272,8 @@ Todas foram cometidas de verdade na execução que gerou este processo. Passe po
|
|
|
258
272
|
| 8 | Assumir tema claro | quem revisa fica desconfortável | escuro desde a entrega 1 |
|
|
259
273
|
| 9 | Script de verificação improvisado | falso positivo/negativo em massa | rodar o instrumento oficial como controle |
|
|
260
274
|
| 10 | Cor lida uma vez no load | tinta escura sobre fundo escuro após trocar tema | ler no momento do uso |
|
|
275
|
+
| 11 | Estado batizado com o título da US | `state-name` == `edge-title` | nome literal do PRD ou lacuna |
|
|
276
|
+
| 12 | `open == 0` sem frase que explique `[S]` | trilho fecha com zero transição órfã e nenhuma frase dizendo por quê | se `open` for zero, escreva a frase explicando — contagem zero sem explicação é tão suspeita quanto contagem alta |
|
|
261
277
|
|
|
262
278
|
## Formato do relatório final
|
|
263
279
|
|
|
@@ -268,7 +284,7 @@ Entregue sempre, e nesta ordem:
|
|
|
268
284
|
3. **Transições** modeladas.
|
|
269
285
|
4. **Lacunas**: cada uma nomeada, com o que o PRD não define.
|
|
270
286
|
5. **Contestações**: cada uma com a afirmação do montante, a evidência, e o que ela contradiz. Se houver contestação, diga explicitamente que **parou** e o que não foi construído.
|
|
271
|
-
6. **Verificação**: saída literal do auditor (refs/quebradas, tokens declarados × definidos), gates exercitados no browser, erros de console, viewports medidos.
|
|
287
|
+
6. **Verificação**: em modo 2A, saída literal do auditor (refs/quebradas, tokens declarados × definidos), gates exercitados no browser, erros de console, viewports medidos. Em modo 2B, saída literal do `audit-ciclo-modelagem.py` + auditor de refs + diff de pares; "gates exercitados" vira "N/A — modo 2B", escrito, nunca omitido.
|
|
272
288
|
7. **O que você NÃO fez** e por quê.
|
|
273
289
|
|
|
274
290
|
Nunca reporte verificação em prosa ("os tokens estão ok"). Cole o número que o instrumento devolveu.
|
|
@@ -51,14 +51,22 @@ Se requisitos não satisfeitos, MUST report bloqueio antes de iniciar.
|
|
|
51
51
|
Este agente é **project-agnostic**. O dispatch prompt do orchestrator MUST conter:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
|
-
PROJECT_NAME: <
|
|
55
|
-
DESIGN_SYSTEM_REPO: <absolute path>
|
|
56
|
-
DELIVERABLES_REPO: <absolute path>
|
|
54
|
+
PROJECT_NAME: <slug do projeto>
|
|
55
|
+
DESIGN_SYSTEM_REPO: <absolute path — injetado pelo despacho>
|
|
56
|
+
DELIVERABLES_REPO: <absolute path — injetado pelo despacho>
|
|
57
57
|
TARGET_DECK: <subdir name dentro de DELIVERABLES_REPO>
|
|
58
58
|
TARGET_AUDIENCE: <self-service | live-demo | hybrid>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
**Falhar alto, não improvisar (S513, decisão do PM na S512).** Antes de qualquer leitura:
|
|
62
|
+
`test -d "$DESIGN_SYSTEM_REPO" && test -d "$DELIVERABLES_REPO/$TARGET_DECK"` (Invariante 6 —
|
|
63
|
+
probe, não inferência). Se **qualquer campo faltar** ou **qualquer path não existir no disco**,
|
|
64
|
+
MUST **parar** e devolver ao orquestrador uma contestação nomeando o campo e o valor recebido
|
|
65
|
+
(`AskUserQuestion` não está no teu conjunto de tools — não tente). NEVER hardcode paths; NEVER
|
|
66
|
+
completar campo ausente com valor lembrado de outra corrida. A definição distribuída deste
|
|
67
|
+
agente **não carrega default de cliente nenhum** — foi assim que um prefixo morto
|
|
68
|
+
(`~/desenvolvimento/trc`, árvore apagada na S497) sobreviveu quatro releases no plugin
|
|
69
|
+
instalado (medido S510, 4 ocorrências na 3.37.0 e na 3.37.1).
|
|
62
70
|
|
|
63
71
|
## Hard Rules — Design System Discipline
|
|
64
72
|
|
|
@@ -68,7 +76,7 @@ Se qualquer campo faltar, MUST `AskUserQuestion` antes de proceder. NEVER hardco
|
|
|
68
76
|
2. MUST Read `$DESIGN_SYSTEM_REPO/components/<relevantes>.md` para componentes em uso
|
|
69
77
|
3. MUST Read `$DESIGN_SYSTEM_REPO/anti-patterns.md` (8 itens "NÃO fazer")
|
|
70
78
|
4. MUST diff `theme/styles/tokens.css` (deck) vs `tokens.md` (SD) — reportar divergências
|
|
71
|
-
5. MUST documentar overrides com comentário inline `/*
|
|
79
|
+
5. MUST documentar overrides com comentário inline `/* <PROJECT_NAME> override: <rationale> */`
|
|
72
80
|
|
|
73
81
|
### MUST NOT (Forbidden)
|
|
74
82
|
|
|
@@ -177,7 +185,7 @@ ${DELIVERABLES_REPO}/${TARGET_DECK}/.cobaia-evidence/
|
|
|
177
185
|
└── COBAIA-REPORT.md # report estruturado final
|
|
178
186
|
```
|
|
179
187
|
|
|
180
|
-
MUST NOT salvar PNGs em outros locais (raiz do orchestrator repo, raiz do
|
|
188
|
+
MUST NOT salvar PNGs em outros locais (raiz do orchestrator repo, raiz do repo de `$DELIVERABLES_REPO`, /tmp). Tudo dentro de `.cobaia-evidence/`. Cleanup é dever do agent, não do orchestrator.
|
|
181
189
|
|
|
182
190
|
### MUST validate exports
|
|
183
191
|
|
|
@@ -368,7 +376,7 @@ Use quando precisar grounding em doc autoritativa de Slidev/Vue/Tailwind antes d
|
|
|
368
376
|
|
|
369
377
|
Standard workflow quando dispatched:
|
|
370
378
|
|
|
371
|
-
1. **Validate dispatch context** — paths, target deck, audience.
|
|
379
|
+
1. **Validate dispatch context** — paths, target deck, audience; `test -d` em cada path. Faltando ou inexistente: **pare e conteste** (ver "Falhar alto" acima).
|
|
372
380
|
2. **Read SD canonical** — `tokens.md`, `anti-patterns.md`, components relevantes (batch parallel)
|
|
373
381
|
3. **Read deck current state** — `slides.md`, theme overrides, current `tokens.css`
|
|
374
382
|
4. **Diff deck vs SD** — gerar TOKEN-SYNC-REPORT.md se divergences existirem
|
|
@@ -500,6 +508,11 @@ MAY write ephemeral observations to `.claude/agent-memory/presentation-engineer/
|
|
|
500
508
|
|
|
501
509
|
## Examples
|
|
502
510
|
|
|
511
|
+
> Os dois exemplos abaixo são de corridas reais de um cliente (TRC, S174). Os **paths absolutos
|
|
512
|
+
> foram removidos** na S513: a árvore que citavam não existe desde a S497, e exemplo com path
|
|
513
|
+
> absoluto numa definição distribuída vira default por leitura. O despacho injeta; a definição
|
|
514
|
+
> não carrega.
|
|
515
|
+
|
|
503
516
|
### Example 1: Slide deck bug fix (S174 cobaia)
|
|
504
517
|
|
|
505
518
|
**Dispatch prompt (mock):**
|
|
@@ -508,8 +521,8 @@ MAY write ephemeral observations to `.claude/agent-memory/presentation-engineer/
|
|
|
508
521
|
Fix bugs visuais no slide deck TRC S174 antes do showcase ao vivo com TRC.
|
|
509
522
|
|
|
510
523
|
PROJECT_NAME: trc
|
|
511
|
-
DESIGN_SYSTEM_REPO:
|
|
512
|
-
DELIVERABLES_REPO:
|
|
524
|
+
DESIGN_SYSTEM_REPO: <absolute path do DS do cliente — injetado pelo despacho>
|
|
525
|
+
DELIVERABLES_REPO: <absolute path do repo de deliverables — injetado pelo despacho>
|
|
513
526
|
TARGET_DECK: slide-deck
|
|
514
527
|
TARGET_AUDIENCE: live-demo
|
|
515
528
|
|
|
@@ -552,8 +565,8 @@ Dados de produção (já conhecidos):
|
|
|
552
565
|
Create new TRC one-pager deck for Phase 2 partnership pitch.
|
|
553
566
|
|
|
554
567
|
PROJECT_NAME: trc
|
|
555
|
-
DESIGN_SYSTEM_REPO:
|
|
556
|
-
DELIVERABLES_REPO:
|
|
568
|
+
DESIGN_SYSTEM_REPO: <absolute path do DS do cliente — injetado pelo despacho>
|
|
569
|
+
DELIVERABLES_REPO: <absolute path do repo de deliverables — injetado pelo despacho>
|
|
557
570
|
TARGET_DECK: one-pager-phase2
|
|
558
571
|
TARGET_AUDIENCE: hybrid (executive readers + presented sessions)
|
|
559
572
|
|
|
@@ -619,6 +619,23 @@ function truncateSummary(summary: string, cap: number): string {
|
|
|
619
619
|
return summary.length > cap ? `${summary.slice(0, cap)}…` : summary;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
/**
|
|
623
|
+
* TD-318 (S514): the ONE field formatMemoryContext dereferences per entry.
|
|
624
|
+
* `readMemoryIndex`'s guard requires only `file:` (S480 contract, pinned by
|
|
625
|
+
* memory-index-silent-failure.test.ts case 5), so an entry with `file:` and
|
|
626
|
+
* no `summary:` reaches here — and `truncateSummary(undefined)` threw,
|
|
627
|
+
* the caller's try/catch swallowed it, and the WHOLE block vanished:
|
|
628
|
+
* measured on trc-platform-mobile, 1 of 140 entries → 0 injected, no
|
|
629
|
+
* INDISPONÍVEL line (S480 guards the read, not the format). One bad line
|
|
630
|
+
* must not zero the good ones beside it.
|
|
631
|
+
*/
|
|
632
|
+
function hasRenderableSummary(entry: MemoryEntry): boolean {
|
|
633
|
+
return typeof entry.summary === 'string';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Header line of the warning appended when entries were skipped (TD-318). */
|
|
637
|
+
export const MEMORY_SKIPPED_WARNING_PREFIX = '- [AVISO] ';
|
|
638
|
+
|
|
622
639
|
function countByType(list: MemoryEntry[]): Record<string, number> {
|
|
623
640
|
const counts: Record<string, number> = {};
|
|
624
641
|
for (const e of list) {
|
|
@@ -647,32 +664,44 @@ function countByType(list: MemoryEntry[]): Record<string, number> {
|
|
|
647
664
|
* survived TASK-008's filter/quota, independent of the source index size.
|
|
648
665
|
*/
|
|
649
666
|
export function formatMemoryContext(selected: MemoryEntry[], allEntries: MemoryEntry[]): string {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
667
|
+
// TD-318: split BEFORE anything dereferences `summary`. Skipped entries
|
|
668
|
+
// are named in a trailing warning line so the author sees what to fix —
|
|
669
|
+
// the previous behaviour was a swallowed throw and an empty block.
|
|
670
|
+
const renderable = selected.filter(hasRenderableSummary);
|
|
671
|
+
const skipped = selected.filter((e) => !hasRenderableSummary(e));
|
|
672
|
+
const warning =
|
|
673
|
+
skipped.length > 0
|
|
674
|
+
? `${MEMORY_SKIPPED_WARNING_PREFIX}${String(skipped.length)} entrada(s) sem 'summary:' pulada(s) ` +
|
|
675
|
+
`— conserte o índice: ${skipped.map((e) => e.file).join(', ')}`
|
|
676
|
+
: '';
|
|
677
|
+
|
|
678
|
+
if (renderable.length === 0) return warning ? `[MEMORY-CONTEXT] 0 memórias\n${warning}` : '';
|
|
679
|
+
|
|
680
|
+
const selectedCounts = countByType(renderable);
|
|
653
681
|
const totalCounts = countByType(allEntries);
|
|
654
682
|
const perTypeSummary = MEMORY_TYPES.map(
|
|
655
683
|
(t) => `${t}: ${selectedCounts[t] ?? 0}/${totalCounts[t] ?? 0}`
|
|
656
684
|
).join(', ');
|
|
657
685
|
|
|
658
686
|
const headerBase =
|
|
659
|
-
`[MEMORY-CONTEXT] ${
|
|
687
|
+
`[MEMORY-CONTEXT] ${renderable.length} de ${allEntries.length} memórias ` +
|
|
660
688
|
`(${perTypeSummary}); critério: cota mínima de 2 por tipo não-feedback + ` +
|
|
661
689
|
`prioridade feedback→recência no restante`;
|
|
662
690
|
|
|
663
|
-
const availableForLines =
|
|
691
|
+
const availableForLines =
|
|
692
|
+
AGGREGATE_CHAR_BUDGET - headerBase.length - SUFFIX_RESERVE - (warning ? warning.length + 1 : 0);
|
|
664
693
|
const lines: string[] = [];
|
|
665
694
|
let used = 0;
|
|
666
695
|
let droppedByBudget = 0;
|
|
667
696
|
|
|
668
|
-
for (let i = 0; i <
|
|
669
|
-
const entry =
|
|
697
|
+
for (let i = 0; i < renderable.length; i++) {
|
|
698
|
+
const entry = renderable[i]!;
|
|
670
699
|
const name = entry.file.replace(/\.md$/, '');
|
|
671
700
|
const truncated = truncateSummary(entry.summary, PER_ENTRY_SUMMARY_CHAR_CAP);
|
|
672
701
|
const line = `- [${entry.type}] ${name}: ${truncated}`;
|
|
673
702
|
|
|
674
703
|
if (used + line.length + 1 > availableForLines) {
|
|
675
|
-
droppedByBudget =
|
|
704
|
+
droppedByBudget = renderable.length - i;
|
|
676
705
|
break;
|
|
677
706
|
}
|
|
678
707
|
lines.push(line);
|
|
@@ -684,7 +713,49 @@ export function formatMemoryContext(selected: MemoryEntry[], allEntries: MemoryE
|
|
|
684
713
|
? `${headerBase}; +${droppedByBudget} truncadas por orçamento de caracteres`
|
|
685
714
|
: headerBase;
|
|
686
715
|
|
|
687
|
-
return [header, ...lines].join('\n');
|
|
716
|
+
return [header, ...lines, warning].filter(Boolean).join('\n');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ─── Memory Index Validation (TD-318) ────────────────────
|
|
720
|
+
|
|
721
|
+
/** One defect found by `validateMemoryIndexEntries`. */
|
|
722
|
+
export interface MemoryIndexDefect {
|
|
723
|
+
/** 0-based position in the index; the file name when the entry has one. */
|
|
724
|
+
position: number;
|
|
725
|
+
file: string | null;
|
|
726
|
+
/** Field names that are missing or of the wrong type. */
|
|
727
|
+
missing: string[];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Full-shape validator for a memory index, meant to FAIL LOUD in CI/lint
|
|
732
|
+
* (TD-318) — the read-side guard (`isWellFormedMemoryEntry`) is
|
|
733
|
+
* deliberately minimal (S480), so this is where an author learns that an
|
|
734
|
+
* entry is missing `summary:` BEFORE it reaches a session. Checks every
|
|
735
|
+
* field the injection path dereferences: `file`, `type`, `summary`,
|
|
736
|
+
* `last_accessed` (strings) and `tags` (array). Pure: takes the parsed
|
|
737
|
+
* entries (either on-disk form, already unwrapped), returns defects, never
|
|
738
|
+
* throws.
|
|
739
|
+
*/
|
|
740
|
+
export function validateMemoryIndexEntries(entries: unknown[]): MemoryIndexDefect[] {
|
|
741
|
+
const defects: MemoryIndexDefect[] = [];
|
|
742
|
+
entries.forEach((value, position) => {
|
|
743
|
+
if (typeof value !== 'object' || value === null) {
|
|
744
|
+
defects.push({ position, file: null, missing: ['(não é um objeto)'] });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const e = value as Record<string, unknown>;
|
|
748
|
+
const missing: string[] = [];
|
|
749
|
+
for (const field of ['file', 'type', 'summary', 'last_accessed']) {
|
|
750
|
+
const v = e[field];
|
|
751
|
+
if (typeof v !== 'string' || v.length === 0) missing.push(field);
|
|
752
|
+
}
|
|
753
|
+
if (!Array.isArray(e.tags)) missing.push('tags');
|
|
754
|
+
if (missing.length > 0) {
|
|
755
|
+
defects.push({ position, file: typeof e.file === 'string' ? e.file : null, missing });
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
return defects;
|
|
688
759
|
}
|
|
689
760
|
|
|
690
761
|
// ─── Memory Index Reading ────────────────────────────────
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* security-gate-core.ts — lógica PURA do gate honesto de commit (S505, ficha 3).
|
|
3
|
+
*
|
|
4
|
+
* Separado de `security-gate-honest.ts` de propósito: aquele arquivo tem um
|
|
5
|
+
* `main()` de topo que aguarda stdin, e importá-lo num teste trava o runner.
|
|
6
|
+
* A convenção anterior desta casa contornava isso RE-IMPLEMENTANDO a lógica
|
|
7
|
+
* dentro do teste (ver `tests/unit/hooks/post-implement.test.ts`), o que
|
|
8
|
+
* duplica a regra e deixa as duas cópias divergirem em silêncio — a família
|
|
9
|
+
* TD-232 que este repo persegue. Extrair o núcleo resolve os dois problemas:
|
|
10
|
+
* o teste importa o que RODA, e não uma cópia dele.
|
|
11
|
+
*
|
|
12
|
+
* Nada aqui faz IO. O `git show` e a saída do hook ficam no arquivo do hook.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Abaixo disto, um veredito de L2 é fallback disfarçado, não análise. */
|
|
16
|
+
export const L2_LATENCY_FLOOR_MS = 400;
|
|
17
|
+
|
|
18
|
+
/** O que a L1 cobre — declarado, porque cobertura não-dita vira cobertura presumida. */
|
|
19
|
+
export const L1_COVERAGE =
|
|
20
|
+
'literais de credencial no diff (chaves de provedor, tokens, blocos PEM, URLs com senha)';
|
|
21
|
+
|
|
22
|
+
export type Verdict =
|
|
23
|
+
| { state: 'clean'; layer: string; detail: string }
|
|
24
|
+
| { state: 'findings'; layer: string; detail: string; hits: Finding[] }
|
|
25
|
+
| { state: 'not-analyzed'; layer: string; reason: string };
|
|
26
|
+
|
|
27
|
+
export interface Finding {
|
|
28
|
+
rule: string;
|
|
29
|
+
file: string;
|
|
30
|
+
line: number;
|
|
31
|
+
evidence: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────
|
|
35
|
+
// L1 — padrões. Estreitos de propósito: cada regra que dispara
|
|
36
|
+
// falso positivo em massa treina quem lê a ignorar, e um gate
|
|
37
|
+
// ignorado é um gate desligado com aparência de ligado.
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
export const SECRET_RULES: { rule: string; re: RegExp }[] = [
|
|
40
|
+
{ rule: 'aws-access-key-id', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
41
|
+
{ rule: 'anthropic-api-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
|
|
42
|
+
{ rule: 'openai-api-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
|
|
43
|
+
{ rule: 'github-token', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
44
|
+
{ rule: 'slack-token', re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/ },
|
|
45
|
+
{ rule: 'google-api-key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
46
|
+
{ rule: 'npm-token', re: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
47
|
+
{ rule: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
|
|
48
|
+
{ rule: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
49
|
+
{ rule: 'url-with-password', re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s:@/]{6,}@/i },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Marcas que indicam placeholder e não segredo real. Sem isto o gate grita em
|
|
54
|
+
* cima do `.env.example` e ensina o leitor a ignorá-lo.
|
|
55
|
+
*/
|
|
56
|
+
const PLACEHOLDER =
|
|
57
|
+
/(?:x{6,}|\.{3,}|<[^>]{2,}>|\$\{|EXAMPLE|PLACEHOLDER|YOUR_|CHANGEME|REDACTED|FAKE|DUMMY|\bTODO\b)/i;
|
|
58
|
+
|
|
59
|
+
/** Mascara a evidência: o gate não pode ser o veículo que vaza o segredo que achou. */
|
|
60
|
+
export function maskEvidence(s: string): string {
|
|
61
|
+
const t = s.trim().slice(0, 80);
|
|
62
|
+
return t.length <= 12 ? '***' : `${t.slice(0, 6)}…${t.slice(-4)} (${String(t.length)} ch)`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Varre um diff unificado. Só linhas ADICIONADAS (`+`) — uma linha removida
|
|
67
|
+
* já está no histórico e culpar o commit que a REMOVE inverte o sinal.
|
|
68
|
+
*/
|
|
69
|
+
export function scanDiff(diff: string): Finding[] {
|
|
70
|
+
const out: Finding[] = [];
|
|
71
|
+
let file = '(desconhecido)';
|
|
72
|
+
let lineNo = 0;
|
|
73
|
+
|
|
74
|
+
for (const raw of diff.split('\n')) {
|
|
75
|
+
if (raw.startsWith('+++ b/')) {
|
|
76
|
+
file = raw.slice(6);
|
|
77
|
+
lineNo = 0;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (raw.startsWith('@@')) {
|
|
81
|
+
const m = /\+(\d+)/.exec(raw);
|
|
82
|
+
lineNo = m ? parseInt(m[1], 10) - 1 : 0;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!raw.startsWith('+') || raw.startsWith('+++')) continue;
|
|
86
|
+
lineNo += 1;
|
|
87
|
+
|
|
88
|
+
const content = raw.slice(1);
|
|
89
|
+
if (PLACEHOLDER.test(content)) continue;
|
|
90
|
+
|
|
91
|
+
for (const { rule, re } of SECRET_RULES) {
|
|
92
|
+
const hit = re.exec(content);
|
|
93
|
+
if (hit) out.push({ rule, file, line: lineNo, evidence: maskEvidence(hit[0]) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detecta `git commit` — e NÃO `git commit --dry-run`, que não cria commit.
|
|
101
|
+
*
|
|
102
|
+
* Aceita opções globais com valor separado (`git -c user.name=x commit`), que a
|
|
103
|
+
* primeira versão desta função deixava passar: o `-c` consome o traço, mas o
|
|
104
|
+
* `user.name=x` seguinte não, e o `commit` deixava de casar. Pego por teste.
|
|
105
|
+
*
|
|
106
|
+
* LIMITAÇÃO CONHECIDA, declarada em vez de escondida: isto é casamento de
|
|
107
|
+
* texto, não parsing de shell. Um comando que apenas CONTENHA a frase
|
|
108
|
+
* (`ls "git commit"`) dispara o gate, que então descreve o HEAD corrente — um
|
|
109
|
+
* commit que não é daquele comando. É ruído, não risco: toda saída nomeia o que
|
|
110
|
+
* analisou, então o leitor vê sobre o que o veredito fala. Parsear shell de
|
|
111
|
+
* verdade custa mais do que o defeito que evita.
|
|
112
|
+
*/
|
|
113
|
+
export function isCommitCommand(command: string): boolean {
|
|
114
|
+
return /git\s+(?:-\S+(?:\s+\S+)?\s+)*commit\b/.test(command) && !/--dry-run\b/.test(command);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─────────────────────────────────────────────────────────────
|
|
118
|
+
// Núcleo
|
|
119
|
+
// ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/** O diff mais a declaração de COMO foi obtido — cobertura não-dita vira cobertura presumida. */
|
|
122
|
+
export interface DiffSource {
|
|
123
|
+
text: string;
|
|
124
|
+
/** Ex.: "git show HEAD" · "git show -m --first-parent HEAD (merge de 2 pais)". */
|
|
125
|
+
how: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface GateDeps {
|
|
129
|
+
getDiff: () => DiffSource;
|
|
130
|
+
/** L2 devolve null enquanto não houver credencial decidida (ficha 3, item d). */
|
|
131
|
+
runSemantic: (diff: string) => { hits: Finding[]; elapsedMs: number } | null;
|
|
132
|
+
l2UnavailableReason: () => string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function evaluate(deps: GateDeps): Verdict[] {
|
|
136
|
+
const verdicts: Verdict[] = [];
|
|
137
|
+
|
|
138
|
+
let src: DiffSource;
|
|
139
|
+
try {
|
|
140
|
+
src = deps.getDiff();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
// A falha de leitura do diff é NÃO ANALISEI para TODAS as camadas.
|
|
143
|
+
// Este é o ponto exato em que o plugin medido diria "limpo".
|
|
144
|
+
const reason = `não consegui ler o diff do commit (${(e as Error).message.slice(0, 80)})`;
|
|
145
|
+
return [
|
|
146
|
+
{ state: 'not-analyzed', layer: 'L1', reason },
|
|
147
|
+
{ state: 'not-analyzed', layer: 'L2', reason },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const diff = src.text;
|
|
152
|
+
if (!diff.trim()) {
|
|
153
|
+
// Distinguir POR QUE veio vazio: um merge sem conflito e um `git show` que
|
|
154
|
+
// falhou pedem leituras diferentes, e medida que não separa estados que
|
|
155
|
+
// pedem ações diferentes é medida errada (regra de desenho da S480).
|
|
156
|
+
const reason =
|
|
157
|
+
`o diff veio vazio via ${src.how} — nada para analisar. ` + 'Isto NÃO é o mesmo que limpo';
|
|
158
|
+
return [
|
|
159
|
+
{ state: 'not-analyzed', layer: 'L1', reason },
|
|
160
|
+
{ state: 'not-analyzed', layer: 'L2', reason },
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── L1 ──
|
|
165
|
+
const hits = scanDiff(diff);
|
|
166
|
+
const cover = `${L1_COVERAGE} · via ${src.how}`;
|
|
167
|
+
verdicts.push(
|
|
168
|
+
hits.length
|
|
169
|
+
? { state: 'findings', layer: 'L1', detail: cover, hits }
|
|
170
|
+
: { state: 'clean', layer: 'L1', detail: cover }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── L2 ──
|
|
174
|
+
const sem = deps.runSemantic(diff);
|
|
175
|
+
if (sem === null) {
|
|
176
|
+
verdicts.push({ state: 'not-analyzed', layer: 'L2', reason: deps.l2UnavailableReason() });
|
|
177
|
+
} else if (sem.elapsedMs < L2_LATENCY_FLOOR_MS) {
|
|
178
|
+
// A guarda de latência. Não confia no veredito: análise semântica de um
|
|
179
|
+
// diff não fecha nesse tempo, então isto é fallback, não resultado.
|
|
180
|
+
verdicts.push({
|
|
181
|
+
state: 'not-analyzed',
|
|
182
|
+
layer: 'L2',
|
|
183
|
+
reason:
|
|
184
|
+
`devolveu veredito em ${String(sem.elapsedMs)} ms, abaixo do piso de ${String(L2_LATENCY_FLOOR_MS)} ms — ` +
|
|
185
|
+
'tratado como fallback, não como análise',
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
verdicts.push(
|
|
189
|
+
sem.hits.length
|
|
190
|
+
? { state: 'findings', layer: 'L2', detail: 'análise semântica', hits: sem.hits }
|
|
191
|
+
: { state: 'clean', layer: 'L2', detail: 'análise semântica' }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return verdicts;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A linha ÚNICA que o humano vê (`systemMessage`), contra o texto por camada que
|
|
200
|
+
* só chega ao modelo (`additionalContext`).
|
|
201
|
+
*
|
|
202
|
+
* Achado do Manager na S505, e é o coração da ficha: **falhar alto significa a
|
|
203
|
+
* PESSOA ver.** Um gate cujo aviso só alcança o modelo tem o mesmo efeito
|
|
204
|
+
* prático do plugin que ele substitui — ninguém é avisado. Por isso a saída sai
|
|
205
|
+
* pelos dois canais.
|
|
206
|
+
*
|
|
207
|
+
* Reporta o PIOR estado, porque é o único que muda o que a pessoa faz agora:
|
|
208
|
+
* achados > nada analisado > cobertura parcial > tudo analisado e limpo.
|
|
209
|
+
*/
|
|
210
|
+
export function renderShort(verdicts: Verdict[]): string {
|
|
211
|
+
const findings = verdicts.filter((v) => v.state === 'findings');
|
|
212
|
+
if (findings.length) {
|
|
213
|
+
const n = findings.reduce((a, v) => a + (v.state === 'findings' ? v.hits.length : 0), 0);
|
|
214
|
+
const layers = findings.map((v) => v.layer).join('+');
|
|
215
|
+
return `SECURITY-GATE: ${String(n)} achado(s) em ${layers} — confira antes de publicar`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const analyzed = verdicts.filter((v) => v.state !== 'not-analyzed').map((v) => v.layer);
|
|
219
|
+
const skipped = verdicts.filter((v) => v.state === 'not-analyzed').map((v) => v.layer);
|
|
220
|
+
|
|
221
|
+
if (!analyzed.length) {
|
|
222
|
+
return 'SECURITY-GATE: NENHUMA camada analisou o commit — isto NÃO é "limpo", é ausência de análise';
|
|
223
|
+
}
|
|
224
|
+
if (skipped.length) {
|
|
225
|
+
return `SECURITY-GATE: cobertura PARCIAL — ${analyzed.join('+')} analisou, ${skipped.join('+')} não`;
|
|
226
|
+
}
|
|
227
|
+
return `SECURITY-GATE: ${analyzed.join('+')} analisou, sem achados`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renderiza. Nunca produz a frase "no security issues found" nem equivalente
|
|
232
|
+
* global: o veredito é sempre por camada, sempre com a cobertura ao lado.
|
|
233
|
+
*/
|
|
234
|
+
export function render(verdicts: Verdict[]): string {
|
|
235
|
+
const lines = ['[SECURITY-GATE] Revisão do commit — veredito por camada:'];
|
|
236
|
+
|
|
237
|
+
for (const v of verdicts) {
|
|
238
|
+
if (v.state === 'clean') {
|
|
239
|
+
lines.push(` ${v.layer} ANALISADO — LIMPO · cobertura: ${v.detail}`);
|
|
240
|
+
} else if (v.state === 'findings') {
|
|
241
|
+
lines.push(
|
|
242
|
+
` ${v.layer} ANALISADO — ${String(v.hits.length)} ACHADO(S) · cobertura: ${v.detail}`
|
|
243
|
+
);
|
|
244
|
+
for (const h of v.hits.slice(0, 10)) {
|
|
245
|
+
lines.push(` ${h.rule} · ${h.file}:${String(h.line)} · ${h.evidence}`);
|
|
246
|
+
}
|
|
247
|
+
if (v.hits.length > 10) lines.push(` … e mais ${String(v.hits.length - 10)}`);
|
|
248
|
+
} else {
|
|
249
|
+
lines.push(` ${v.layer} NÃO ANALISEI — ${v.reason}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const analyzed = verdicts.filter((v) => v.state !== 'not-analyzed').map((v) => v.layer);
|
|
254
|
+
const skipped = verdicts.filter((v) => v.state === 'not-analyzed').map((v) => v.layer);
|
|
255
|
+
|
|
256
|
+
if (!analyzed.length) {
|
|
257
|
+
lines.push(
|
|
258
|
+
' ⚠ NENHUMA camada analisou este commit. Isto NÃO é "limpo" — é ausência de informação.'
|
|
259
|
+
);
|
|
260
|
+
} else if (skipped.length) {
|
|
261
|
+
lines.push(
|
|
262
|
+
` ⚠ Cobertura PARCIAL: ${analyzed.join('+')} rodou, ${skipped.join('+')} não. ` +
|
|
263
|
+
'O que não foi analisado não está limpo — está por analisar.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (verdicts.some((v) => v.state === 'findings')) {
|
|
268
|
+
lines.push(' → Confira antes de publicar. O commit local já existe; nada foi bloqueado.');
|
|
269
|
+
}
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|