@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/cli.mjs +68 -3
  3. package/bin/lib/copy.mjs +505 -67
  4. package/bin/lib/materialize.mjs +99 -1
  5. package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
  6. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  7. package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
  8. package/docs/adr/README.md +2 -0
  9. package/docs/code-style.md +47 -8
  10. package/docs/protocol.md +1 -1
  11. package/docs/releasing.md +7 -0
  12. package/package.json +1 -1
  13. package/registry/instructions/opus.md +3 -1
  14. package/registry/skills/build-opus-ui/SKILL.md +5 -3
  15. package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
  16. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
  17. package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
  18. package/registry/templates/app/pnpm-workspace.yaml +1 -1
  19. package/src/core/system-actor.ts +1 -1
  20. package/src/ui/components/patterns/list.tsx +1 -1
  21. package/src/ui/docs/content/action-list.md +1 -1
  22. package/src/ui/docs/content/actions.md +1 -1
  23. package/src/ui/docs/content/auth.md +3 -3
  24. package/src/ui/docs/content/cli.md +6 -0
  25. package/src/ui/docs/content/communication.md +10 -8
  26. package/src/ui/docs/content/events.md +13 -13
  27. package/src/ui/docs/content/installation.md +4 -0
  28. package/src/ui/docs/content/log.md +1 -1
  29. package/src/ui/docs/content/queue.md +2 -2
  30. package/src/ui/docs/content/router.md +1 -1
  31. package/src/ui/docs/content/scheduler.md +15 -15
  32. package/src/ui/docs/content/testing.md +4 -4
  33. package/src/vite/design.ts +1 -1
@@ -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
- reconcileInstructions(target, 'CLAUDE.md', mode, errors, changes)
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('caixa:fechar')`. O helper não lê a provenance, que qualquer código servidor pode
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(contrato, resposta)`, e a resposta pode ser um valor ou uma função tipada
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.
@@ -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).
@@ -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`, `line` e `role` devem identificar exatamente uma entrada;
52
- `text` é um refinamento opcional. Mudanças que invalidam o seletor reprovam a extração. Metadata
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 exatamente um diagnóstico por `source`, `line` e `field`, registra `reason` e
149
- `reference` auditáveis e usa um destes contratos:
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
- As declarações aparecem em `dynamicSurfaces` no inventário. Se o source mudar e o seletor não
157
- encontrar exatamente um diagnóstico, `opus copy` falha. Assim, a classificação permanece
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('caixa:fechar')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "28.0.0",
3
+ "version": "29.0.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 seletor exato, motivo e referência: `runtime-data` para valores sem autoria
93
- editorial; `message-template` com `role` e texto-modelo para mensagens interpoladas. Não excluir
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`. Conferir
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 CLAUDE.md) a cada
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
@@ -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('caixa:fechar')`.
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=chave:dir` (omitido quando é o default do contrato) ·
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=chave:dir&status=…&view=board&cols=a,b,c&limit=25`.
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/pedidos.ts` quanto `design/scenarios/pedidos/lista.ts`. O cenário vence a
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 notaEmitir = bindAction(notaEmitirContract, {
68
- authorize: (ctx) => ctx.can('nota.emitir'),
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('nota.cancelar', nota))) {
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(nome)`) gera o diagnóstico `content` habitual e
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.chave`), nunca passado adiante, espalhado ou
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, a
101
- linha e o campo exibido pelo diagnóstico. `runtime-data` registra valores como nomes,
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 corresponder a um único diagnóstico reprova o
106
- gate e precisa ser revisto. Diagnósticos estruturais ou de transformação visual não podem ser
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
- "line": 42,
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
- "line": 58,
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 // 'nota.emitida'
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
- `nota.*`, não são reconhecidos: assine `*` e filtre no handler.
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 notaEmitir = bindAction(notaEmitirContract, {
55
- emits: ['nota.emitida'],
54
+ export const invoiceIssue = bindAction(invoiceIssueContract, {
55
+ emits: ['invoice.issued'],
56
56
  handler: async (ctx, input) => {
57
- const nota = await repo.create(input)
57
+ const invoice = await repo.create(input)
58
58
  // O runtime preenche id, timestamp, actor, tenant, source e trace.
59
- await ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
60
- return nota
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 notaAvisarFinanceiro = defineReactionContract<{ id: string; valor: number }>({
90
- name: 'nota.avisar-financeiro',
91
- on: 'nota.emitida',
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 notaAvisarFinanceiroImpl = bindReaction(notaAvisarFinanceiro, {
96
+ export const invoiceNotifyFinanceImpl = bindReaction(invoiceNotifyFinance, {
97
97
  handler: async (ctx, event) => {
98
- ctx.log.info('nota emitida', { id: event.data.id, valor: event.data.valor })
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: [notaAvisarFinanceiroImpl] })`
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