@softize/opus 28.0.0 → 30.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 (52) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/bin/cli.mjs +68 -3
  3. package/bin/lib/check.mjs +86 -10
  4. package/bin/lib/copy.mjs +505 -67
  5. package/bin/lib/materialize.mjs +99 -1
  6. package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
  7. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  8. package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
  9. package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
  10. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  11. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  12. package/docs/adr/README.md +6 -0
  13. package/docs/code-style.md +47 -8
  14. package/docs/protocol.md +1 -1
  15. package/docs/releasing.md +7 -0
  16. package/package.json +1 -1
  17. package/registry/instructions/opus.md +3 -1
  18. package/registry/skills/build-opus-ui/SKILL.md +6 -4
  19. package/registry/skills/build-opus-ui/references/evaluations.md +10 -1
  20. package/registry/skills/build-opus-ui/references/ui-patterns.md +10 -2
  21. package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
  22. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
  23. package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
  24. package/registry/templates/app/pnpm-workspace.yaml +1 -1
  25. package/src/core/system-actor.ts +1 -1
  26. package/src/core/types.ts +1 -1
  27. package/src/ui/components/patterns/list.tsx +19 -10
  28. package/src/ui/components/patterns/page.tsx +27 -0
  29. package/src/ui/components/patterns/structural-header.tsx +25 -3
  30. package/src/ui/components/patterns/surface.tsx +36 -8
  31. package/src/ui/components/primitives/dialog.tsx +74 -42
  32. package/src/ui/components/primitives/drawer.tsx +69 -42
  33. package/src/ui/components/primitives/table.tsx +3 -3
  34. package/src/ui/docs/content/action-list.md +15 -8
  35. package/src/ui/docs/content/actions.md +1 -1
  36. package/src/ui/docs/content/auth.md +3 -3
  37. package/src/ui/docs/content/cli.md +6 -0
  38. package/src/ui/docs/content/communication.md +10 -8
  39. package/src/ui/docs/content/dialog.md +39 -0
  40. package/src/ui/docs/content/drawer.md +36 -0
  41. package/src/ui/docs/content/events.md +13 -13
  42. package/src/ui/docs/content/installation.md +4 -0
  43. package/src/ui/docs/content/log.md +1 -1
  44. package/src/ui/docs/content/page.md +30 -1
  45. package/src/ui/docs/content/queue.md +2 -2
  46. package/src/ui/docs/content/router.md +1 -1
  47. package/src/ui/docs/content/scheduler.md +15 -15
  48. package/src/ui/docs/content/surface.md +41 -8
  49. package/src/ui/docs/content/table.md +7 -0
  50. package/src/ui/docs/content/testing.md +4 -4
  51. package/src/ui/react.tsx +4 -0
  52. 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
@@ -17,6 +17,9 @@
17
17
  `Surface`.
18
18
  - **Não altera:** `Page`, `Dialog`, `Drawer` e `Pane`, que mantêm famílias, slots e mídia próprios.
19
19
 
20
+ A ADR 0063 substitui parcialmente esta decisão: `SurfaceMeta` passa a representar a faixa
21
+ de metadados; o contador na linha do título passa a usar `SurfaceCount`.
22
+
20
23
  ## Contexto
21
24
 
22
25
  Quem monta uma tela precisa delimitar e nomear um trecho dela: uma seção da página, um grupo de
@@ -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.
@@ -0,0 +1,68 @@
1
+ # ADR 0062 — Listas contidas e texto flexível
2
+
3
+ Data: 2026-10-15
4
+ Status: Aceita
5
+
6
+ ## Contexto
7
+
8
+ Descrições sem quebra faziam uma tabela de Habilidades ocupar 92,21rem dentro de
9
+ 63,13rem disponíveis. A coluna implícita do grid de resultados do ActionList
10
+ crescia pelo conteúdo; o contêiner da tabela crescia junto e não oferecia sua
11
+ rolagem horizontal. A largura mínima da página não limita a largura dos filhos.
12
+
13
+ ## Decisão
14
+
15
+ Table contém sua largura e quebra texto entre palavras por padrão. `overflow-wrap:
16
+ break-word` preserva a largura intrínseca das palavras, evitando que uma descrição
17
+ comprida faça uma coluna como “Inativo” quebrar no meio. Identificadores sem espaços
18
+ podem exigir rolagem interna; `wrap-anywhere` fica disponível como opção explícita.
19
+ Classes explícitas continuam permitindo
20
+ células sem quebra. ActionList limita sua coluna de resultados ao espaço
21
+ recebido e permite encolhimento ao longo da cadeia de flex/grid.
22
+
23
+ `fit` preserva valores compactos sem quebra, como números e datas. O cabeçalho
24
+ de uma coluna declarada pode quebrar entre palavras. Textos flexíveis recebem o
25
+ restante do espaço. Não há redução de tipografia nem ocultação automática de colunas.
26
+
27
+ A barra de filtros pode ocupar mais de uma linha, mantendo a ordem dos controles.
28
+ Uma tabela que ainda exceda a largura disponível rola dentro da lista: em flow,
29
+ no contêiner da tabela; em fill, na região de resultados que também ancora o
30
+ cabeçalho fixo. O rodapé e a toolbar não acompanham essa rolagem.
31
+
32
+ ## Alternativas e consequências
33
+
34
+ Largura fixa e repartição igual sacrificariam espaço das colunas textuais mesmo
35
+ quando controles curtos não precisam dele. Ocultar o excesso perderia dados e
36
+ ações. Regras CSS no aplicativo duplicariam o comportamento do pacote.
37
+
38
+ A quebra pode aumentar a altura das linhas. Consumidores com células compostas
39
+ continuam responsáveis por permitir encolhimento dos filhos flex e por declarar
40
+ larguras mínimas somente quando necessárias. Comparativos com colunas mínimas
41
+ explícitas continuam usando rolagem horizontal.
42
+
43
+ ## Verificação
44
+
45
+ Testes em Chromium medem contenção, textos longos, filtros, valores compactos e
46
+ rolagem nos modos flow e fill, incluindo a posição do cabeçalho após rolar.
47
+ Os testes estão em `tests/browser/action-list-width.test.tsx` e cobrem também
48
+ palavras curtas ao lado de descrições longas e tokens sem espaços.
49
+
50
+ ## Revisão da distribuição entre colunas — 2026-10-15
51
+
52
+ Na validação de Consumo, quatro colunas `fit` ficavam comprimidas enquanto a coluna
53
+ textual recebia cerca de 70% da largura. A largura mínima forçada fazia o navegador
54
+ quebrar títulos mesmo com espaço disponível. Retirar essa restrição reduziu a coluna
55
+ textual para cerca de 38% e acomodou os cabeçalhos numa linha, no mesmo contêiner.
56
+
57
+ Colunas de dados passam a participar da distribuição automática da tabela, considerando
58
+ cabeçalhos e valores. `fit` continua preservando valores sem quebra, mas não impõe
59
+ largura mínima artificial. Cabeçalhos podem quebrar quando o espaço realmente faltar.
60
+ Seleção, ações de linha e células contendo apenas um switch continuam compactas.
61
+ O switch pode ser filho direto ou estar envolvido pelo TooltipTrigger. A regra usa a
62
+ estrutura do controle, sem depender do nome da coluna; células com conteúdo misto
63
+ continuam participando da distribuição natural.
64
+
65
+ Não se distribui largura igualmente nem se mede texto em JavaScript. O algoritmo
66
+ nativo considera o conteúdo e o espaço recebido; a contenção e a rolagem descritas
67
+ acima permanecem. Os testes de navegador cobrem várias colunas numéricas ao lado de
68
+ texto, tanto com espaço suficiente quanto em contêiner estreito, em flow e fill.
@@ -0,0 +1,44 @@
1
+ # ADR 0063 — Metadados das superfícies em região própria
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-15.
5
+ - **Complementa:** ADR 0060.
6
+ - **Substitui parcialmente:** ADR 0045 quanto à posição e função de SurfaceMeta.
7
+
8
+ ## Contexto
9
+
10
+ Badges, autoria e datas disputam espaço com o título ou usam o subheading como container.
11
+ A descrição é orientação textual próxima ao heading; metadados precisam de espaçamento
12
+ próprio e quebra de linha sem alterar o tamanho ou o significado do título.
13
+
14
+ ## Decisão
15
+
16
+ PageMeta, DrawerMeta, DialogMeta e SurfaceMeta compõem uma faixa opcional após a linha
17
+ que contém título, descrição e ações. Não são headings nem descrições acessíveis dos modais.
18
+ As três primeiras famílias aceitam Meta em Intro ou Header compacto; Surface mantém
19
+ Meta em SurfaceHeader, pois não possui Intro. Meta exige título na mesma composição.
20
+ Dialog em modo de alerta não aceita Meta; informações necessárias à resposta pertencem ao corpo.
21
+
22
+ O intervalo superior é de 0,75rem; itens usam 0,75rem na horizontal e 0,5rem entre linhas.
23
+ A faixa permite quebra, conserva a tipografia dos badges e não insere separadores nem
24
+ busca dados. Sem Meta não há espaço reservado. Uma composição aceita no máximo um Meta.
25
+ As formas curtas de Page e Surface aceitam a prop meta. Fieldset preserva a legend
26
+ restrita a título e mídia decorativa; seus metadados pertencem ao corpo.
27
+
28
+ SurfaceCount preserva o contador ao lado do título e recebe count na forma curta.
29
+ SurfaceMeta deixa de ocupar essa posição: consumidores de contadores explícitos migram
30
+ para SurfaceCount. Consumidores de contexto editorial permanecem em SurfaceMeta.
31
+
32
+ ## Alternativas
33
+
34
+ Usar Description conservaria a mistura de prosa e metadados. Colocar wrappers livres no
35
+ Body repetiria espaçamento nos consumidores e afastaria o contexto da identidade. Manter
36
+ SurfaceMeta como contador faria o mesmo nome expressar funções diferentes entre famílias.
37
+
38
+ ## Verificação
39
+
40
+ Testes renderizados e checker cobrem posição, unicidade, pai permitido e título obrigatório.
41
+ Contadores permanecem ao lado do título. Testes no navegador conferem intervalo e quebra
42
+ nas famílias, incluindo headers compactos com ações. As evidências pertencem aos arquivos
43
+ `tests/ui/surface-meta.test.tsx`, `tests/browser/surface-meta.test.tsx` e
44
+ `tests/check/check.test.ts`. Publicação e adoção dependem de autorização de entrega.
@@ -14,3 +14,9 @@ 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).
19
+
20
+ - [ADR 0062 — Listas contidas e texto flexível](0062-listas-contidas-e-texto-flexivel.md).
21
+
22
+ - [ADR 0063 — Metadados das superfícies em região própria](0063-metadados-das-superficies.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": "30.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
@@ -65,7 +65,7 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
65
65
  relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
66
66
  com justificativa e cobertura explícitas.
67
67
  11. Em uma página centrada em coleção, compor `Surface + ActionList`: título e criação ficam em
68
- extremos opostos do `SurfaceHeader`; a criação usa botão textual `default`, sem ícone, com
68
+ extremos opostos do `SurfaceHeader`; a criação usa botão textual `sm`, sem ícone, com
69
69
  `Criar recurso`. Busca, filtros, atualização, estados e paginação permanecem no `ActionList`.
70
70
  Reservar `toolbarActions` a operações ligadas ao recorte atual.
71
71
  12. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
@@ -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
@@ -7,7 +7,7 @@
7
7
  outra em `PageShell` com navegação contextual acima de `Page > PageHeader + PageBody`, garantindo
8
8
  exatamente um `PageTitle` no header, e uma coleção nomeada com
9
9
  `Surface > SurfaceHeader + SurfaceBody > ActionList`. Provar que criação fica em
10
- `SurfaceActions`, no extremo oposto ao título, como botão textual `default` sem ícone e com
10
+ `SurfaceActions`, no extremo oposto ao título, como botão textual `sm` sem ícone e com
11
11
  `Criar recurso`, enquanto uma operação dependente do recorte usa `toolbarActions`; incluir
12
12
  `ActionFilterBar` separado do renderer, `ItemGroup` para
13
13
  uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
@@ -84,3 +84,12 @@ da label, duplicação entre hint/help e perda de `aria-describedby` quando apar
84
84
  IntroActions, nenhum header vazio na Page e transição de loading para conteúdo sem título duplicado.
85
85
  - Reprovar intro depois de texto, título simultâneo em header e intro, ações da superfície no título
86
86
  ou adaptação de Pane para que ele volte a possuir anatomia visual.
87
+
88
+ ## Metadados e contadores
89
+
90
+ - Execução: compor um detalhe com título, descrição e badges em PageMeta, DrawerMeta e DialogMeta;
91
+ conferir a faixa separada em Header compacto e Intro. Em Surface, conferir que SurfaceCount
92
+ fica na linha do título e SurfaceMeta abaixo dela. Migrar um contador antigo e uma descrição
93
+ antiga sem alterar suas funções; provar a equivalência das formas curtas com `meta`.
94
+ - Reprova: contador em SurfaceMeta, metadados em Description, dois Meta, Meta sem título,
95
+ Meta em fieldset ou DialogMeta em alerta.
@@ -36,8 +36,8 @@
36
36
  recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
37
37
  `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
38
38
  - `Surface` delimita uma região e segue a anatomia `Surface > SurfaceHeader (SurfaceMedia,
39
- SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
40
- Os slots nunca ficam soltos. `title`, `description`, `count`, `media` e `actions` são a abreviação
39
+ SurfaceTitle, SurfaceCount, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
40
+ Os slots nunca ficam soltos. `title`, `description`, `count`, `meta`, `media` e `actions` são a abreviação
41
41
  para o caso direto e não podem ser misturados ao header explícito. Em `level={1}`, ações da
42
42
  coleção ficam no extremo oposto ao título; a criação usa botão textual `sm`, sem ícone, com
43
43
  `Criar recurso`.
@@ -45,6 +45,14 @@ SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + S
45
45
  hierarquia semântica do heading. `Surface framed` aceita `layout="sectioned"`; declare
46
46
  `SurfaceBody flush` quando o filho direto já possui recuo interno, como `DetailGroup` com
47
47
  divisores, lista ou tabela. `as="fieldset"` cria a `legend` nativa sem variante de moldura.
48
+ - `PageMeta`, `DialogMeta` e `DrawerMeta` recebem badges, datas e autoria em faixa própria
49
+ após título, descrição e ações, dentro de Header compacto ou Intro da mesma família.
50
+ `SurfaceMeta` ocupa essa faixa em `SurfaceHeader`; `SurfaceCount` mantém o contador na linha
51
+ do título. Ao migrar, conferir manualmente os antigos `SurfaceMeta`: contadores passam a
52
+ `SurfaceCount`, frases de orientação a `SurfaceDescription`, e metadados permanecem em Meta.
53
+ Cada composição aceita um Meta e exige título. Page e Surface aceitam `meta` na forma curta;
54
+ fieldset e Dialog em `mode="alert"` não aceitam Meta. Informações necessárias à decisão
55
+ permanecem no corpo ou junto à ação correspondente.
48
56
  - Uma página centrada em coleção compõe `Surface + ActionList`. O `SurfaceHeader` recebe título e
49
57
  criação; o `ActionList` mantém busca, filtros, atualização, resultados, estados e paginação.
50
58
  `toolbarActions` fica reservado a operações que dependem do recorte atual.
@@ -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 {
package/src/core/types.ts CHANGED
@@ -714,7 +714,7 @@ export interface ListColumnSpec {
714
714
  /** Header clicável (asc ↔ desc). Convenção: a UI escreve `sort: '<key>:<dir>'` no
715
715
  * input; o handler implementa o orderBy. */
716
716
  sortable?: boolean
717
- /** Coluna encolhe ao conteúdo (não expande). */
717
+ /** Preserva valores sem quebra; a largura considera também o cabeçalho e o espaço disponível. */
718
718
  fit?: boolean
719
719
  /** Nasce oculta (candidata ao column picker). */
720
720
  hidden?: boolean