@softize/opus 28.0.0 → 29.0.0
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/CHANGELOG.md +58 -0
- package/bin/cli.mjs +68 -3
- package/bin/lib/copy.mjs +505 -67
- package/bin/lib/materialize.mjs +99 -1
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
- package/docs/adr/README.md +2 -0
- package/docs/code-style.md +47 -8
- package/docs/protocol.md +1 -1
- package/docs/releasing.md +7 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -1
- package/registry/skills/build-opus-ui/SKILL.md +5 -3
- package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
- package/registry/templates/app/pnpm-workspace.yaml +1 -1
- package/src/core/system-actor.ts +1 -1
- package/src/ui/components/patterns/list.tsx +1 -1
- package/src/ui/docs/content/action-list.md +1 -1
- package/src/ui/docs/content/actions.md +1 -1
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/cli.md +6 -0
- package/src/ui/docs/content/communication.md +10 -8
- package/src/ui/docs/content/events.md +13 -13
- package/src/ui/docs/content/installation.md +4 -0
- package/src/ui/docs/content/log.md +1 -1
- package/src/ui/docs/content/queue.md +2 -2
- package/src/ui/docs/content/router.md +1 -1
- package/src/ui/docs/content/scheduler.md +15 -15
- package/src/ui/docs/content/testing.md +4 -4
- package/src/vite/design.ts +1 -1
package/bin/lib/materialize.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const REGISTRY = join(PACKAGE_ROOT, 'registry')
|
|
|
27
27
|
const BLOCK_SOURCE = 'registry/instructions/opus.md'
|
|
28
28
|
const BLOCK_END = '<!-- softize-managed:end @softize/opus -->'
|
|
29
29
|
const MANAGED_ROOTS = ['.agents/skills', '.claude/skills', '.claude/hooks', '.github/workflows', '.githooks/pre-push.d']
|
|
30
|
+
const CLAUDE_IMPORT = '@AGENTS.md'
|
|
30
31
|
|
|
31
32
|
const normalizedLineEndings = (content) => content.replace(/\r\n?/gu, '\n')
|
|
32
33
|
const hash = (content) => createHash('sha256').update(normalizedLineEndings(content)).digest('hex')
|
|
@@ -533,6 +534,103 @@ function reconcileInstructions(root, file, mode, errors, changes) {
|
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
|
|
537
|
+
// Blocos gerenciados de qualquer pacote. Um marcador de início ilegível não identifica dono e
|
|
538
|
+
// conta como texto do projeto. Mesmo critério da Base (@softize/base, ADR 0006).
|
|
539
|
+
function managedBlocks(content) {
|
|
540
|
+
const blocks = []
|
|
541
|
+
let cursor = 0
|
|
542
|
+
for (const match of content.matchAll(/<!-- softize-managed:start (\{[^\n]+\}) -->/g)) {
|
|
543
|
+
let meta
|
|
544
|
+
try {
|
|
545
|
+
meta = JSON.parse(match[1])
|
|
546
|
+
} catch {
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
if (typeof meta?.package !== 'string') continue
|
|
550
|
+
const openEnd = match.index + match[0].length
|
|
551
|
+
const endMarker = `<!-- softize-managed:end ${meta.package} -->`
|
|
552
|
+
const endAt = content.indexOf(endMarker, openEnd)
|
|
553
|
+
if (match.index < cursor || endAt === -1 || content.slice(openEnd, endAt).includes('softize-managed:start')) return null
|
|
554
|
+
const end = endAt + endMarker.length
|
|
555
|
+
blocks.push({ start: match.index, end, package: meta.package, text: content.slice(match.index, end) })
|
|
556
|
+
cursor = end
|
|
557
|
+
}
|
|
558
|
+
return blocks
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Texto do projeto fora dos blocos gerenciados e da linha de import, normalizado para comparar o
|
|
562
|
+
// CLAUDE.md legado com o AGENTS.md sem depender de fim de linha ou de linhas em branco.
|
|
563
|
+
function freeInstructions(content, blocks) {
|
|
564
|
+
let rest = ''
|
|
565
|
+
let cursor = 0
|
|
566
|
+
for (const block of blocks) {
|
|
567
|
+
rest += content.slice(cursor, block.start)
|
|
568
|
+
cursor = block.end
|
|
569
|
+
}
|
|
570
|
+
rest += content.slice(cursor)
|
|
571
|
+
return normalizedLineEndings(rest)
|
|
572
|
+
.split('\n')
|
|
573
|
+
.map((line) => line.trimEnd())
|
|
574
|
+
.filter((line) => line !== CLAUDE_IMPORT)
|
|
575
|
+
.join('\n')
|
|
576
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
577
|
+
.trim()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// O CLAUDE.md só importa o AGENTS.md, que concentra as instruções para qualquer agente. O Opus
|
|
581
|
+
// remove dele apenas o próprio bloco; blocos de outros pacotes continuam depois do import até que
|
|
582
|
+
// o pacote dono os remova, para que versões antigas e novas convivam sem reescrever o arquivo uma
|
|
583
|
+
// da outra. Texto do projeto que só existe no CLAUDE.md é preservado e reprovado.
|
|
584
|
+
function reconcileClaudeImport(root, mode, errors, changes) {
|
|
585
|
+
const destination = 'CLAUDE.md'
|
|
586
|
+
const pending =
|
|
587
|
+
`${destination}: deve conter a linha ${CLAUDE_IMPORT}, que importa o AGENTS.md, seguida apenas dos ` +
|
|
588
|
+
'blocos de outros pacotes, quando houver; rode opus setup.'
|
|
589
|
+
const currentFile = readMaterializedFile(root, destination, errors)
|
|
590
|
+
if (currentFile === null) return
|
|
591
|
+
const current = currentFile.exists ? currentFile.content : ''
|
|
592
|
+
const own = findBlock(current)
|
|
593
|
+
const blocks = managedBlocks(current)
|
|
594
|
+
if (own?.malformed || blocks === null) {
|
|
595
|
+
errors.push(`${destination}: bloco gerenciado malformado.`)
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
if (own !== null && (own.owner?.package !== PACKAGE_NAME || hash(own.body) !== own.owner.sha256)) {
|
|
599
|
+
errors.push(`${destination}: bloco Opus foi editado ou perdeu sua identificação de origem; preservado.`)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const free = freeInstructions(current, blocks)
|
|
604
|
+
if (free !== '') {
|
|
605
|
+
const agentsFile = readMaterializedFile(root, 'AGENTS.md', errors)
|
|
606
|
+
if (agentsFile === null) return
|
|
607
|
+
const agents = agentsFile.exists ? agentsFile.content : ''
|
|
608
|
+
if (free !== freeInstructions(agents, managedBlocks(agents) ?? [])) {
|
|
609
|
+
errors.push(
|
|
610
|
+
`${destination}: tem instruções que não estão no AGENTS.md; preservado. O CLAUDE.md passa a conter só ` +
|
|
611
|
+
`${CLAUDE_IMPORT}, então mova essas instruções para o AGENTS.md e rode opus setup.`,
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const foreign = blocks.filter((block) => block.package !== PACKAGE_NAME).map((block) => normalizedLineEndings(block.text))
|
|
618
|
+
const expected = `${[CLAUDE_IMPORT, ...foreign].join('\n\n')}\n`
|
|
619
|
+
if (currentFile.exists && managedTextEquivalent(current, expected)) return
|
|
620
|
+
if (mode === 'check') errors.push(pending)
|
|
621
|
+
else if (
|
|
622
|
+
replaceMaterializedFile(
|
|
623
|
+
root,
|
|
624
|
+
destination,
|
|
625
|
+
currentFile.exists ? withProjectLineEndings(expected, current) : expected,
|
|
626
|
+
currentFile,
|
|
627
|
+
errors,
|
|
628
|
+
)
|
|
629
|
+
) {
|
|
630
|
+
changes.push(`${currentFile.exists ? 'atualizado' : 'criado'} ${destination} como import do AGENTS.md`)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
536
634
|
function isOpusHook(hook) { return typeof hook?.command === 'string' && hook.command.includes('.claude/hooks/opus-check-on-stop.mjs') }
|
|
537
635
|
|
|
538
636
|
function isPlainObject(value) {
|
|
@@ -792,7 +890,7 @@ export function materializeOpus(root, mode = 'setup', options = {}) {
|
|
|
792
890
|
removeObsolete(target, expected, mode, errors, changes)
|
|
793
891
|
detectLegacyArtifacts(target, errors)
|
|
794
892
|
reconcileInstructions(target, 'AGENTS.md', mode, errors, changes)
|
|
795
|
-
|
|
893
|
+
reconcileClaudeImport(target, mode, errors, changes)
|
|
796
894
|
reconcileSettings(target, mode, errors, changes)
|
|
797
895
|
configureGit(target, mode, errors, changes)
|
|
798
896
|
return { ok: errors.length === 0, errors, changes, expected: [...expected.keys()] }
|
|
@@ -45,7 +45,7 @@ HTTP sem usuário; o acesso do sistema é opt-in por action e negado por padrão
|
|
|
45
45
|
`{ kind: 'schedule', scheduleName }` — ou `background` com essa provenance original. O helper
|
|
46
46
|
`isSystemActor(ctx)`, exportado pelo core, responde se o ator é o sistema, para que um
|
|
47
47
|
`authorize` possa aceitá-lo de forma explícita: `(ctx) => isSystemActor(ctx) ||
|
|
48
|
-
ctx.can('
|
|
48
|
+
ctx.can('ledger:close')`. O helper não lê a provenance, que qualquer código servidor pode
|
|
49
49
|
preencher: ele consulta um registro interno dos contextos que o próprio runtime criou para o
|
|
50
50
|
sistema. `requires` continua declarativo: o sistema não detém permissões, e o manifest mostra
|
|
51
51
|
`automatable` ao lado de `permission`.
|
|
@@ -54,7 +54,7 @@ Forças em tensão:
|
|
|
54
54
|
- uma action ligada a um handler real (`bindAction`) mantém esse handler, como hoje.
|
|
55
55
|
3. **Cenários são registrados fora do contrato, pelo nome da action.** O plugin ganha a opção
|
|
56
56
|
`scenarios`, com um ou mais globs. Os módulos casados exportam cenários criados por
|
|
57
|
-
`defineDesignScenario(
|
|
57
|
+
`defineDesignScenario(contract, response)`, e a resposta pode ser um valor ou uma função tipada
|
|
58
58
|
pela entrada e pela saída do contrato. O cenário vence a resposta padrão. Um cenário para uma
|
|
59
59
|
action inexistente é erro visível no preview, não um silêncio.
|
|
60
60
|
4. **O Opus não decide onde os cenários ficam.** A opção não tem default dentro do código da
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ADR 0061 — O `CLAUDE.md` importa o `AGENTS.md`
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-15.
|
|
5
|
+
- **Acompanha:** ADR 0006 do `@softize/base`, que define o mesmo formato.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
O `opus setup` escrevia o bloco de instruções do Opus no `AGENTS.md`, lido pelo Codex, e no
|
|
10
|
+
`CLAUDE.md`, lido pelo Claude. A Base fazia o mesmo com o bloco dela. Fora dos blocos, cada projeto
|
|
11
|
+
mantinha à mão as próprias regras nos dois arquivos, e as duas cópias divergiam: na GrandBrasil,
|
|
12
|
+
quatro convenções existiram só no `AGENTS.md` por doze dias, e as sessões do Claude trabalharam sem
|
|
13
|
+
elas.
|
|
14
|
+
|
|
15
|
+
A documentação do Claude Code recomenda, para quem usa as duas ferramentas, um `CLAUDE.md` com a
|
|
16
|
+
linha `@AGENTS.md`. A Base adotou esse formato na ADR 0006. Como o `CLAUDE.md` recebe blocos dos
|
|
17
|
+
dois pacotes, e cada projeto adota as versões deles em momentos diferentes, o Opus precisa seguir
|
|
18
|
+
a mesma regra para que um `setup` não desfaça o do outro.
|
|
19
|
+
|
|
20
|
+
## Decisão
|
|
21
|
+
|
|
22
|
+
O `AGENTS.md` é o único arquivo com instruções. O `opus setup` mantém nele o bloco do Opus e grava
|
|
23
|
+
o `CLAUDE.md` como a linha `@AGENTS.md`, sem marcador.
|
|
24
|
+
|
|
25
|
+
Ao reconciliar o `CLAUDE.md`, o Opus remove só o próprio bloco, quando íntegro. Blocos de outros
|
|
26
|
+
pacotes ficam depois da linha do import, separados por uma linha em branco, até que o pacote dono
|
|
27
|
+
os remova. O texto do projeto fora dos blocos e do import é comparado com o do `AGENTS.md`, com fim
|
|
28
|
+
de linha, espaços no fim das linhas e linhas em branco seguidas normalizados: se for igual, é
|
|
29
|
+
cópia e sai; se for diferente, `setup` e `opus check` preservam o arquivo e pedem para mover as
|
|
30
|
+
instruções para o `AGENTS.md`. Os critérios de bloco, de linha do import e de normalização são os
|
|
31
|
+
da Base, e as mensagens seguem as dela, trocando o comando por `opus setup`.
|
|
32
|
+
|
|
33
|
+
## Consequências
|
|
34
|
+
|
|
35
|
+
- Projeto com instruções só no `CLAUDE.md` precisa movê-las para o `AGENTS.md` antes de o `setup`
|
|
36
|
+
concluir. Por isso a mudança sai numa versão major.
|
|
37
|
+
- Enquanto um projeto usa uma Base anterior à ADR 0006, o `CLAUDE.md` fica com o import e o bloco
|
|
38
|
+
da Base. O conteúdo da Base aparece duas vezes para o Claude, pelo import e pelo bloco, até a
|
|
39
|
+
adoção da Base nova; o arquivo não oscila entre os dois `setup`.
|
|
40
|
+
- Um bloco do Opus editado à mão ou um bloco gerenciado malformado no `CLAUDE.md` são preservados e
|
|
41
|
+
reprovados, como antes.
|
package/docs/adr/README.md
CHANGED
|
@@ -14,3 +14,5 @@ estar documentado na superfície correspondente, sem exigir a reconstrução des
|
|
|
14
14
|
- [ADR 0059 — Subheadings orientam sem carregar regras de negócio](0059-subheadings-orient-without-carrying-business-rules.md).
|
|
15
15
|
|
|
16
16
|
- [ADR 0060 — Composição de superfícies com cabeçalho e intro](0060-surface-composition-separates-chrome-and-intro.md).
|
|
17
|
+
|
|
18
|
+
- [ADR 0061 — O `CLAUDE.md` importa o `AGENTS.md`](0061-claude-md-imports-agents-md.md).
|
package/docs/code-style.md
CHANGED
|
@@ -48,8 +48,12 @@ dinâmica, importada sem fallback ou composta de forma opaca reprova a extraçã
|
|
|
48
48
|
lacunas conhecidas visíveis em vez de produzir um inventário verde e incompleto.
|
|
49
49
|
|
|
50
50
|
Transformações visuais não observáveis e exceções ficam explícitas em `base.json`, usando um
|
|
51
|
-
seletor estável e revisável. `source`, `
|
|
52
|
-
|
|
51
|
+
seletor estável e revisável. `source`, `role` e `text` selecionam o texto pelo conteúdo: a
|
|
52
|
+
declaração vale para toda ocorrência desse texto com esse papel no arquivo e continua válida
|
|
53
|
+
quando linhas entram ou saem acima dela. Um seletor que não encontra o texto reprova a extração,
|
|
54
|
+
porque o texto mudou ou saiu do arquivo. O formato anterior, com `line` no lugar de `text` (ou
|
|
55
|
+
junto dele), ainda é aceito e seleciona exatamente uma entrada naquela linha; `opus copy
|
|
56
|
+
--reanchor` converte esses seletores para o formato por conteúdo. Metadata
|
|
53
57
|
pode complementar a análise, mas nunca sobrescrever uma transformação já observada no JSX —
|
|
54
58
|
`transform: "none"` não mascara `className="uppercase"`. O Opus valida o shape `COPYnnn`; a
|
|
55
59
|
Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada regra:
|
|
@@ -63,16 +67,16 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
|
|
|
63
67
|
"transforms": [
|
|
64
68
|
{
|
|
65
69
|
"source": "src/account.ts",
|
|
66
|
-
"line": 12,
|
|
67
70
|
"role": "button",
|
|
71
|
+
"text": "Confirmar",
|
|
68
72
|
"transform": "uppercase"
|
|
69
73
|
}
|
|
70
74
|
],
|
|
71
75
|
"exemptions": [
|
|
72
76
|
{
|
|
73
77
|
"source": "src/account.ts",
|
|
74
|
-
"line": 18,
|
|
75
78
|
"role": "label",
|
|
79
|
+
"text": "ABC-123",
|
|
76
80
|
"ruleId": "COPY001",
|
|
77
81
|
"kind": "external-requirement",
|
|
78
82
|
"reason": "Código regulatório exibido na grafia oficial.",
|
|
@@ -145,21 +149,56 @@ exigiria adivinhar espaços continuam reprovando.
|
|
|
145
149
|
|
|
146
150
|
Quando o diagnóstico corresponde a dados reais da aplicação ou a uma mensagem interpolada,
|
|
147
151
|
`copy.dynamic` permite classificar somente aquela superfície sem excluir o arquivo. Cada item
|
|
148
|
-
seleciona
|
|
149
|
-
`
|
|
152
|
+
seleciona o diagnóstico por `source`, `field` e `expression` — o trecho de código que o
|
|
153
|
+
extrator não conseguiu resolver; a mensagem do `opus copy` traz os dois valores prontos para
|
|
154
|
+
copiar —,
|
|
155
|
+
registra `reason` e `reference` auditáveis e usa um destes contratos:
|
|
150
156
|
|
|
151
157
|
- `runtime-data` reconhece nomes, contagens, status ou opções fornecidos em runtime. Nenhuma
|
|
152
158
|
entrada editorial é fabricada;
|
|
153
159
|
- `message-template` exige `role` e `text`. O texto-modelo entra no inventário e passa pela
|
|
154
160
|
política da Base, mas não substitui nem altera o conteúdo renderizado.
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
No trecho, espaços e quebras de linha viram um espaço só e a vírgula final antes de `)`, `]` ou
|
|
163
|
+
`}` é ignorada, para a âncora sobreviver à reformatação; trechos com mais de 200 caracteres são
|
|
164
|
+
cortados e terminam em `…`. A declaração vale para toda ocorrência do mesmo trecho no mesmo
|
|
165
|
+
campo do arquivo e não depende da linha em que ele está.
|
|
166
|
+
|
|
167
|
+
As declarações aparecem em `dynamicSurfaces` no inventário, com as linhas atuais de cada
|
|
168
|
+
ocorrência. Se o código mudar e o seletor deixar de encontrar o trecho, `opus copy` falha e lista,
|
|
169
|
+
numa execução, todas as declarações quebradas, cada uma com o arquivo, o campo e os trechos desse
|
|
170
|
+
campo que ainda não têm declaração. Assim, a classificação permanece
|
|
158
171
|
estreita e revisável, sem transformar metadata em uma exclusão silenciosa. O mecanismo aceita
|
|
159
172
|
somente diagnósticos de conteúdo; incerteza estrutural, `className`, `style`, transformação ou
|
|
160
173
|
modo de renderização continua bloqueante. Superfície vazia também não pode ser reclassificada
|
|
161
174
|
como dado de runtime.
|
|
162
175
|
|
|
176
|
+
Declarações escritas antes da versão 29.0.0 usam `line` no lugar de `expression` (ou de `text`,
|
|
177
|
+
nas exceções e transformações) e quebram sempre que alguém edita o arquivo acima delas. Elas
|
|
178
|
+
continuam aceitas, selecionam exatamente um alvo naquela linha e são aplicadas antes das
|
|
179
|
+
declarações por conteúdo, para que uma declaração pontual não seja coberta por uma geral; o
|
|
180
|
+
`opus copy` avisa quantas restam. Para convertê-las, rode `opus copy --reanchor`. O comando
|
|
181
|
+
agrupa as declarações por arquivo e campo (em `copy.dynamic`) ou por arquivo, papel e texto
|
|
182
|
+
(nas exceções e transformações), grava o `base.json` e regenera o inventário:
|
|
183
|
+
|
|
184
|
+
- um grupo só é convertido quando tem exatamente uma declaração para cada alvo. Assim a âncora
|
|
185
|
+
por conteúdo cobre os mesmos alvos que as âncoras por linha cobriam. Se o mesmo texto aparece
|
|
186
|
+
mais vezes do que foi declarado, ou se há um diagnóstico do campo sem declaração, o grupo
|
|
187
|
+
continua por linha e aparece no relatório;
|
|
188
|
+
- dentro do grupo, declarações e alvos são pareados em ordem de linha. Quando o arquivo mudou e
|
|
189
|
+
as âncoras quebraram, o comando lista cada declaração que mudou de linha para revisão no diff;
|
|
190
|
+
- declarações idênticas que passariam a selecionar o mesmo conteúdo viram uma só;
|
|
191
|
+
- duas declarações com motivos diferentes para o mesmo conteúdo continuam por linha, já
|
|
192
|
+
corrigida, porque um seletor por conteúdo cobriria as duas com um único motivo. Para
|
|
193
|
+
convertê-las, unifique o motivo ou mantenha a âncora por linha;
|
|
194
|
+
- os seletores gravados precisam passar na extração do `opus copy`; o que não dá para ancorar com
|
|
195
|
+
segurança fica como está e aparece no relatório.
|
|
196
|
+
|
|
197
|
+
O `--reanchor` não avalia a política de copy da Base. Depois dele, rode `opus copy --check` e
|
|
198
|
+
`base copy check`. Na revisão de uma declaração por conteúdo, vale olhar `dynamicSurfaces[].lines`
|
|
199
|
+
no diff do inventário: uma lista que cresce indica que um trecho genérico, como `title` ou
|
|
200
|
+
`count`, passou a cobrir uma ocorrência nova, que pode ter outra origem.
|
|
201
|
+
|
|
163
202
|
Um `opus copy --check` verde **não significa que toda a copy do produto foi coberta**. Ficam
|
|
164
203
|
fora, deliberadamente:
|
|
165
204
|
|
package/docs/protocol.md
CHANGED
|
@@ -679,7 +679,7 @@ O acesso do sistema é **opt-in por action** e negado por padrão. Sem usuário,
|
|
|
679
679
|
`isSystemActor(ctx)` responde se o ator é o sistema. Ele consulta um registro interno dos contextos que o runtime criou, e não a provenance. Como o sistema não detém permissões, um `authorize` que delega a `ctx.can` o aceita de forma explícita, numa closure — a DSL em string não expressa isso, e `ctx.provenance` não serve como teste de sistema, porque o chamador de `runtime.execute` pode preenchê-la:
|
|
680
680
|
|
|
681
681
|
```ts
|
|
682
|
-
authorize: (ctx) => isSystemActor(ctx) || ctx.can('
|
|
682
|
+
authorize: (ctx) => isSystemActor(ctx) || ctx.can('ledger:close')
|
|
683
683
|
```
|
|
684
684
|
|
|
685
685
|
`requires` continua declarativo; o manifest projeta `automatable` ao lado de `permission`. O audit registra `actor: { id: null, type: 'system' }`. Ver ADR 0028.
|
package/docs/releasing.md
CHANGED
|
@@ -18,6 +18,13 @@ confere essa entrada só DEPOIS do bump, então esquecê-la aborta a release com
|
|
|
18
18
|
alterado — e a tentativa seguinte para na guarda de árvore limpa. Para recuperar, desfaça o bump
|
|
19
19
|
(`git checkout -- package.json`), escreva a entrada, commite e rode de novo.
|
|
20
20
|
|
|
21
|
+
Uma entrada que já está na `origin/main` não muda mais: quem consome o pacote lê o changelog como
|
|
22
|
+
guia de migração, e em 10/09/2026 uma formatação avulsa reescreveu entradas publicadas e mudou o
|
|
23
|
+
sentido de uma delas. Para corrigir uma informação publicada, registre a correção numa entrada
|
|
24
|
+
nova, na próxima versão. A entrada da versão em preparo, enquanto não chega à `main`, pode ser
|
|
25
|
+
ajustada livremente. O pre-push (`.githooks/pre-push.d/01-changelog`) e o `pnpm release` reprovam
|
|
26
|
+
a alteração de uma seção publicada; para conferir antes, rode `pnpm changelog:check` na raiz.
|
|
27
|
+
|
|
21
28
|
Os comandos rodam em `packages/opus`; o `package.json` da raiz não tem `release`.
|
|
22
29
|
|
|
23
30
|
```bash
|
package/package.json
CHANGED
|
@@ -24,7 +24,9 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
|
|
|
24
24
|
e operá-los por `opus seed`; não criar comandos de seed paralelos nem reset implícito.
|
|
25
25
|
- Regenerar o inventário com `opus copy` quando mudar copy em contrato ou componente Opus
|
|
26
26
|
mapeado; o Opus classifica os papéis semânticos e a política universal de `@softize/base`
|
|
27
|
-
valida o conteúdo.
|
|
27
|
+
valida o conteúdo. O inventário também guarda o hash de todo arquivo JS/TS do escopo, mesmo sem
|
|
28
|
+
copy, como os scripts em `scripts/*.mjs`; alterar qualquer um deles também exige `opus copy`,
|
|
29
|
+
e o pre-push reprova o inventário desatualizado.
|
|
28
30
|
- Rodar `opus check`, `opus copy --check` e os gates do projeto depois de alterar actions,
|
|
29
31
|
bindings ou versão do SDK.
|
|
30
32
|
- Quando a default branch tiver adotado uma versão diferente da worktree, reconciliar a
|
|
@@ -89,10 +89,12 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
89
89
|
Rodar `opus check`, testes focados, typecheck e build da superfície. Se a interface alterou
|
|
90
90
|
copy em contrato ou componente Opus mapeado, regenerar com `opus copy` e executar
|
|
91
91
|
`base copy check`. Quando uma superfície mapeada recebe dados da aplicação, usar
|
|
92
|
-
`copy.dynamic` com
|
|
93
|
-
editorial; `message-template` com `role` e
|
|
92
|
+
`copy.dynamic` com `field` e `expression` copiados do diagnóstico, motivo e referência:
|
|
93
|
+
`runtime-data` para valores sem autoria editorial; `message-template` com `role` e
|
|
94
|
+
texto-modelo para mensagens interpoladas. Não usar `line` em declaração nova. Não excluir
|
|
94
95
|
o arquivo inteiro nem contornar o diagnóstico com `aria-label` paralelo. Revisar cada declaração
|
|
95
|
-
dinâmica como fronteira de cobertura, mesmo com os gates verdes
|
|
96
|
+
dinâmica como fronteira de cobertura, mesmo com os gates verdes, inclusive quando
|
|
97
|
+
`dynamicSurfaces[].lines` cresce no diff do inventário.
|
|
96
98
|
Inspecionar visualmente a rota real e validar navegação por URL quando aplicável.
|
|
97
99
|
|
|
98
100
|
## Limites
|
|
@@ -52,7 +52,8 @@ o código não sustenta.
|
|
|
52
52
|
|
|
53
53
|
Executar `node scripts/audit-docs.mjs "$(git rev-parse --show-toplevel)"` a partir do diretório
|
|
54
54
|
desta skill e, depois, os testes do renderer e do catálogo, typecheck, `opus check` e
|
|
55
|
-
`opus copy --check`.
|
|
55
|
+
`opus copy --check`. No repositório do Opus, o teste `tests/scripts/example-language.test.ts`
|
|
56
|
+
reprova identificadores com palavra em português nos exemplos e nas fontes; rode-o também. Conferir
|
|
56
57
|
que todos os previews renderizam, que cada API pública documentada possui sua própria referência,
|
|
57
58
|
que nenhum arquivo publicado ficou órfão e que links e redirects chegam à página esperada. Fazer uma
|
|
58
59
|
passada visual nas páginas alteradas e registrar qualquer limitação que o gate automático não cubra.
|
|
@@ -40,6 +40,13 @@ genérica.
|
|
|
40
40
|
- Depois do código, explicar somente efeitos que não sejam óbvios pela leitura: default aplicado,
|
|
41
41
|
estado controlado, ação automática, limite ou consequência.
|
|
42
42
|
- Usar preview para comportamento visual e bloco de código para integração sem palco executável.
|
|
43
|
+
- Escrever o código dos exemplos em inglês, como o resto do código: variáveis, funções, campos,
|
|
44
|
+
nomes de action, domínios, eventos, schedules, permissões e caminhos de arquivo
|
|
45
|
+
(`ledgerCloseContract`, `ledger.close`, `ledger:close`, `design/scenarios/orders.ts`). Isso vale
|
|
46
|
+
também para o código em crase no meio da prosa. O texto que a pessoa vê — `label`, mensagens,
|
|
47
|
+
descrições, conteúdo JSX — e os comentários continuam em pt-BR. Um exemplo fictício não tem
|
|
48
|
+
vocabulário de domínio estabelecido, então a exceção de domínio das instruções do projeto não se
|
|
49
|
+
aplica a ele.
|
|
43
50
|
|
|
44
51
|
## Famílias e referência
|
|
45
52
|
|
|
@@ -74,6 +81,8 @@ Para cada título, parágrafo, item, exemplo e célula de tabela, verificar:
|
|
|
74
81
|
8. O texto ao redor do exemplo explica intenção e comportamento, sem narrar cada linha do código?
|
|
75
82
|
9. O título permite localizar a informação pela tarefa, inclusive por link direto?
|
|
76
83
|
10. Pontuação, capitalização, concordância e paralelismo estão consistentes?
|
|
84
|
+
11. Os identificadores do exemplo e do código em crase estão em inglês, e o texto exibido à pessoa,
|
|
85
|
+
em pt-BR?
|
|
77
86
|
|
|
78
87
|
Ao terminar cada página, reler a sequência inteira. Linhas corretas isoladamente ainda podem formar
|
|
79
88
|
uma página repetitiva, invertida ou sem progressão.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- language-exempt-file: o caso de avaliação descreve de propósito um exemplo com identificadores em português. -->
|
|
2
|
+
|
|
1
3
|
# Avaliações
|
|
2
4
|
|
|
3
5
|
## Deve disparar
|
|
@@ -32,3 +34,9 @@ Exigir inventário explícito, leitura linha por linha, correção na fonte can
|
|
|
32
34
|
previews, typecheck, gates de copy e confirmação de que não restaram páginas órfãs. Reprovar se a
|
|
33
35
|
execução se limitar às páginas mais longas, fizer apenas busca por padrões ou declarar cobertura
|
|
34
36
|
total sem listar o universo revisado.
|
|
37
|
+
|
|
38
|
+
Num exemplo de agendamento com `caixaFecharContract`, a action `caixa.fechar`, a permissão
|
|
39
|
+
`caixa:fechar` e o rótulo `Fechar caixa`, pedir a revisão da página sem apontar o problema.
|
|
40
|
+
Aprovar somente quando identificadores, action, domínio e permissão passarem para o inglês, o
|
|
41
|
+
rótulo continuar em pt-BR e o gate de idioma passar. Reprovar se o rótulo for traduzido ou se a
|
|
42
|
+
exceção de vocabulário de domínio for usada para manter o exemplo fictício em português.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Permissão de build scripts (o pnpm bloqueia por default): o postinstall do
|
|
2
|
-
# @softize/opus roda o `opus setup` (pin + bloco gerenciado do
|
|
2
|
+
# @softize/opus roda o `opus setup` (pin + bloco gerenciado do AGENTS.md) a cada
|
|
3
3
|
# install; o esbuild (vite) compila o binário nativo.
|
|
4
4
|
allowBuilds:
|
|
5
5
|
'@softize/opus': true
|
package/src/core/system-actor.ts
CHANGED
|
@@ -17,7 +17,7 @@ const systemContexts = new WeakSet<object>()
|
|
|
17
17
|
/**
|
|
18
18
|
* Diz se a action está executando como o sistema: disparada por um schedule, ou no job que ele
|
|
19
19
|
* enfileirou, sem usuário. Use no `authorize` de uma action `automatable` para aceitar o sistema
|
|
20
|
-
* de forma explícita: `authorize: (ctx) => isSystemActor(ctx) || ctx.can('
|
|
20
|
+
* de forma explícita: `authorize: (ctx) => isSystemActor(ctx) || ctx.can('ledger:close')`.
|
|
21
21
|
* Passe o próprio `ctx` recebido: uma cópia, como `{ ...ctx }`, não é reconhecida.
|
|
22
22
|
*/
|
|
23
23
|
export function isSystemActor(ctx: ActionContext): boolean {
|
|
@@ -3058,7 +3058,7 @@ export function ActionList<TInput, TItem>({
|
|
|
3058
3058
|
// URL sync — a serialização padrão do estado da toolbar (querystring compacta)
|
|
3059
3059
|
// =============================================================================
|
|
3060
3060
|
//
|
|
3061
|
-
// Convenção: `q` · `sort=
|
|
3061
|
+
// Convenção: `q` · `sort=key:dir` (omitido quando é o default do contrato) ·
|
|
3062
3062
|
// `period=preset` relativo ou `period=from..to` (o custom é implícito; default
|
|
3063
3063
|
// omitido) · `view` (omitida quando é a primeira) · `cols=a,b,c` (omitida no
|
|
3064
3064
|
// default) · filtros pelo PRÓPRIO nome (multiple → CSV). Quem embala liga:
|
|
@@ -459,7 +459,7 @@ restaurá-lo.
|
|
|
459
459
|
/>
|
|
460
460
|
```
|
|
461
461
|
|
|
462
|
-
Convenção compacta (defaults omitidos): `?q=…&sort=
|
|
462
|
+
Convenção compacta (defaults omitidos): `?q=…&sort=key:dir&status=…&view=board&cols=a,b,c&limit=25`.
|
|
463
463
|
|
|
464
464
|
## Composição dos itens
|
|
465
465
|
|
|
@@ -323,7 +323,7 @@ A resposta é tipada pela entrada e pela saída do contrato; em `list`, é a pá
|
|
|
323
323
|
plugin onde os cenários estão, com caminhos ou globs:
|
|
324
324
|
`opusDesign({ scenarios: 'design/scenarios/**/*.ts' })`. Nos globs, `*` casa parte do nome dentro
|
|
325
325
|
de uma pasta e `**` casa qualquer número de pastas, inclusive nenhuma: o padrão do exemplo carrega
|
|
326
|
-
tanto `design/scenarios/
|
|
326
|
+
tanto `design/scenarios/orders.ts` quanto `design/scenarios/orders/list.ts`. O cenário vence a
|
|
327
327
|
resposta gerada. Um cenário de uma action que não existe, ou dois cenários para a mesma action,
|
|
328
328
|
impedem o runtime de design de subir, e o erro lista os nomes.
|
|
329
329
|
|
|
@@ -64,8 +64,8 @@ const runtime = createRuntime({
|
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
// Na action: `authorize` decide antes do handler, com o contexto já resolvido.
|
|
67
|
-
export const
|
|
68
|
-
authorize: (ctx) => ctx.can('
|
|
67
|
+
export const invoiceIssue = bindAction(invoiceIssueContract, {
|
|
68
|
+
authorize: (ctx) => ctx.can('invoice:issue'),
|
|
69
69
|
handler: async (ctx, input) => repo.create({ ...input, tenantId: ctx.tenantId }),
|
|
70
70
|
})
|
|
71
71
|
```
|
|
@@ -95,7 +95,7 @@ Quando a decisão depende de dado que só o handler conhece, verifique no própr
|
|
|
95
95
|
```ts
|
|
96
96
|
import { error } from '@softize/opus/core'
|
|
97
97
|
|
|
98
|
-
if (!(await ctx.can('
|
|
98
|
+
if (!(await ctx.can('invoice:cancel', invoice))) {
|
|
99
99
|
throw error({ code: 'auth.forbidden', category: 'authorization', message: 'Acesso negado' })
|
|
100
100
|
}
|
|
101
101
|
```
|
|
@@ -82,6 +82,7 @@ opus gen # manifest / openapi / docs / stubs
|
|
|
82
82
|
opus gen --config ./apps/api/opus.config.ts # config fora do diretório atual
|
|
83
83
|
opus gen --output ./custom/gen # pasta de saída (padrão: a do config, ou ./.gen)
|
|
84
84
|
opus copy # inventário semântico de copy dos contratos
|
|
85
|
+
opus copy --reanchor # ancora pelo conteúdo as declarações antigas do base.json
|
|
85
86
|
opus introspect # modelo da estrutura: actions, reactions, schedules e ligações
|
|
86
87
|
opus introspect --json
|
|
87
88
|
```
|
|
@@ -92,6 +93,11 @@ copy da `@softize/base` valida. O caminho vem de `base.json` (`copy.inventory`;
|
|
|
92
93
|
versionado. `--check` compara sem escrever e falha quando o inventário está ausente,
|
|
93
94
|
desatualizado ou contém copy dinâmica não declarada.
|
|
94
95
|
|
|
96
|
+
Declarações de `copy.dynamic`, `copy.exemptions` e `copy.transforms` são ancoradas pelo conteúdo
|
|
97
|
+
(`expression` ou `text`), e não pela linha. `opus copy --reanchor` converte as declarações antigas,
|
|
98
|
+
ancoradas por `line`, grava o `base.json` e lista para revisão as que mudaram de linha ou
|
|
99
|
+
continuam por linha; o detalhe está em `docs/code-style.md` do pacote.
|
|
100
|
+
|
|
95
101
|
`opus introspect` lê o código sem executá-lo e só reconhece `defineAction`, `defineReaction`,
|
|
96
102
|
`defineSchedule` e `defineDataProduct`. Actions do par `defineContract` e `bindAction`, reactions do
|
|
97
103
|
par `defineReactionContract` e `bindReaction` e schedules declarados sem `defineSchedule` ficam fora
|
|
@@ -68,7 +68,7 @@ declaração adicional:
|
|
|
68
68
|
respostas (`alert`, `prompt`, `choose`) entram no inventário como se fossem props de um
|
|
69
69
|
`ActionFormDialog`; o campo do diagnóstico é `dialog.confirm().title`,
|
|
70
70
|
`dialog.choose().actions[0].label` etc. Constantes locais resolvem normalmente. Uma função
|
|
71
|
-
que devolve template (`body: describe(
|
|
71
|
+
que devolve template (`body: describe(name)`) gera o diagnóstico `content` habitual e
|
|
72
72
|
se declara em `copy.dynamic` como `message-template`. Opções construídas em tempo de execução
|
|
73
73
|
ou mutadas depois de declaradas reprovam como estrutura, porque nenhum texto pode ser
|
|
74
74
|
atribuído a elas.
|
|
@@ -80,7 +80,7 @@ declaração adicional:
|
|
|
80
80
|
mesmo arquivo, cada literal do array vira um texto na linha em que foi escrito, em vez de um
|
|
81
81
|
diagnóstico sobre a prop. `filter`, `slice`, `toSorted` e `toReversed` podem ficar entre o
|
|
82
82
|
array e a iteração. O limite é deliberado: o array precisa ser literal e declarado no mesmo
|
|
83
|
-
arquivo; o elemento só pode ser lido (`item.
|
|
83
|
+
arquivo; o elemento só pode ser lido (`item.key`), nunca passado adiante, espalhado ou
|
|
84
84
|
atribuído; o callback não pode receber o próprio array como terceiro parâmetro; e nenhum
|
|
85
85
|
elemento pode ter a chave dinâmica. Arrays importados, derivados por spread ou por
|
|
86
86
|
concatenação continuam gerando o diagnóstico da prop.
|
|
@@ -97,13 +97,15 @@ visual que o protocolo não representa com segurança.
|
|
|
97
97
|
é copy editorial estável; `label` e `confirm.*` são as superfícies editoriais cobertas.
|
|
98
98
|
|
|
99
99
|
Quando uma prop mapeada recebe dados da aplicação, declare a superfície em
|
|
100
|
-
`base.json.copy.dynamic` em vez de excluir o arquivo inteiro. O seletor usa o caminho,
|
|
101
|
-
|
|
100
|
+
`base.json.copy.dynamic` em vez de excluir o arquivo inteiro. O seletor usa o caminho, o campo
|
|
101
|
+
e o trecho de código exibidos pelo diagnóstico, sem a linha: a declaração continua válida quando
|
|
102
|
+
o arquivo muda acima dela. `runtime-data` registra valores como nomes,
|
|
102
103
|
contagens e opções vindas de API sem transformá-los em copy editorial. `message-template`
|
|
103
104
|
acrescenta ao inventário um texto-modelo com papel semântico, para que a parte autoral de uma
|
|
104
105
|
mensagem interpolada continue passando pela política da Base. As declarações não alteram o
|
|
105
|
-
que o React renderiza; um seletor que deixa de
|
|
106
|
-
|
|
106
|
+
que o React renderiza; um seletor que deixa de encontrar o trecho reprova o gate e precisa ser
|
|
107
|
+
revisto. Declarações antigas, ancoradas por `line`, são convertidas com `opus copy --reanchor`.
|
|
108
|
+
Diagnósticos estruturais ou de transformação visual não podem ser
|
|
107
109
|
dispensados por `copy.dynamic`; ausência de conteúdo também continua bloqueante.
|
|
108
110
|
|
|
109
111
|
```json
|
|
@@ -112,7 +114,7 @@ dispensados por `copy.dynamic`; ausência de conteúdo também continua bloquean
|
|
|
112
114
|
"dynamic": [
|
|
113
115
|
{
|
|
114
116
|
"source": "src/sessions.tsx",
|
|
115
|
-
"
|
|
117
|
+
"expression": "sessionOptions",
|
|
116
118
|
"field": "<Select>.options",
|
|
117
119
|
"kind": "runtime-data",
|
|
118
120
|
"reason": "As opções são nomes de sessões retornados pela aplicação.",
|
|
@@ -120,7 +122,7 @@ dispensados por `copy.dynamic`; ausência de conteúdo também continua bloquean
|
|
|
120
122
|
},
|
|
121
123
|
{
|
|
122
124
|
"source": "src/sessions.tsx",
|
|
123
|
-
"
|
|
125
|
+
"expression": "`Excluir ${session.name}?`",
|
|
124
126
|
"field": "dialog.confirm().body",
|
|
125
127
|
"kind": "message-template",
|
|
126
128
|
"role": "dialog-body",
|
|
@@ -17,7 +17,7 @@ interface EventBusAdapter {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface DomainEvent<T = unknown> {
|
|
20
|
-
type: string // '
|
|
20
|
+
type: string // 'invoice.issued'
|
|
21
21
|
id: string
|
|
22
22
|
timestamp: string
|
|
23
23
|
actor: { id: string | null; type?: 'user' | 'system' | 'integration' }
|
|
@@ -39,7 +39,7 @@ const bus = mittEvents() // no mesmo processo do runtime
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
O `mitt` entrega pelo nome exato do evento ou por `*`, que recebe todos. Padrões parciais, como
|
|
42
|
-
`
|
|
42
|
+
`invoice.*`, não são reconhecidos: assine `*` e filtre no handler.
|
|
43
43
|
|
|
44
44
|
## Emitir de uma action
|
|
45
45
|
|
|
@@ -51,13 +51,13 @@ const runtime = createRuntime({
|
|
|
51
51
|
eventBus: bus,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
export const
|
|
55
|
-
emits: ['
|
|
54
|
+
export const invoiceIssue = bindAction(invoiceIssueContract, {
|
|
55
|
+
emits: ['invoice.issued'],
|
|
56
56
|
handler: async (ctx, input) => {
|
|
57
|
-
const
|
|
57
|
+
const invoice = await repo.create(input)
|
|
58
58
|
// O runtime preenche id, timestamp, actor, tenant, source e trace.
|
|
59
|
-
await ctx.emit('
|
|
60
|
-
return
|
|
59
|
+
await ctx.emit('invoice.issued', { id: invoice.id, amount: invoice.amount })
|
|
60
|
+
return invoice
|
|
61
61
|
},
|
|
62
62
|
})
|
|
63
63
|
```
|
|
@@ -86,21 +86,21 @@ no código compartilhado e o efeito, no servidor:
|
|
|
86
86
|
import { bindReaction, defineReactionContract } from '@softize/opus/core'
|
|
87
87
|
|
|
88
88
|
// shared/ — qual evento e o que a reação significa.
|
|
89
|
-
export const
|
|
90
|
-
name: '
|
|
91
|
-
on: '
|
|
89
|
+
export const invoiceNotifyFinance = defineReactionContract<{ id: string; amount: number }>({
|
|
90
|
+
name: 'invoice.notify-finance',
|
|
91
|
+
on: 'invoice.issued',
|
|
92
92
|
description: 'Avisa o financeiro quando uma nota é emitida.',
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
// api/ — o efeito.
|
|
96
|
-
export const
|
|
96
|
+
export const invoiceNotifyFinanceImpl = bindReaction(invoiceNotifyFinance, {
|
|
97
97
|
handler: async (ctx, event) => {
|
|
98
|
-
ctx.log.info('nota emitida', { id: event.data.id,
|
|
98
|
+
ctx.log.info('nota emitida', { id: event.data.id, amount: event.data.amount })
|
|
99
99
|
},
|
|
100
100
|
})
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
Registre a reaction com as actions, em `defineDomain({ reactions: [
|
|
103
|
+
Registre a reaction com as actions, em `defineDomain({ reactions: [invoiceNotifyFinanceImpl] })`
|
|
104
104
|
ou em `runtime.register`. Com reactions registradas, o `start()` exige um `EventBusAdapter` e falha
|
|
105
105
|
com `runtime.eventbus_required` quando ele não existe. `defineReaction` declara tudo num objeto só,
|
|
106
106
|
quando não há separação entre cliente e servidor.
|
|
@@ -25,6 +25,10 @@ pnpm test
|
|
|
25
25
|
O aplicativo inclui um domínio de tarefas, `opus.config.ts`, Vite, React, o tema do Opus e os
|
|
26
26
|
comandos de teste e verificação. O dev server respeita a variável `PORT`. `opus.json` registra a
|
|
27
27
|
versão do SDK adotada pelo app; `setup` materializa as instruções, skills e hooks gerenciados.
|
|
28
|
+
As instruções ficam no `AGENTS.md`, que qualquer agente lê; o `CLAUDE.md` contém só a linha
|
|
29
|
+
`@AGENTS.md`, que as importa para o Claude Code. Escreva as regras do seu projeto no `AGENTS.md`:
|
|
30
|
+
o `setup` recusa um `CLAUDE.md` com instruções que não estão no `AGENTS.md` e pede que elas sejam
|
|
31
|
+
movidas para lá.
|
|
28
32
|
|
|
29
33
|
Quando os testes passarem, continue em [Primeiros passos](/docs/getting-started).
|
|
30
34
|
|