@softize/opus 27.1.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 +113 -0
- package/bin/cli.mjs +68 -3
- package/bin/lib/check.mjs +139 -30
- package/bin/lib/copy.mjs +505 -67
- package/bin/lib/materialize.mjs +99 -1
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
- package/docs/adr/README.md +4 -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 +16 -14
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- 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 +63 -54
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +12 -3
- package/src/ui/docs/content/actions.md +1 -1
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/cli.md +6 -0
- package/src/ui/docs/content/communication.md +10 -8
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/events.md +13 -13
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/installation.md +4 -0
- package/src/ui/docs/content/log.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- 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/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/docs/content/testing.md +4 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
- package/src/vite/design.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,119 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 29.0.0 — 2026-10-15
|
|
11
|
+
|
|
12
|
+
O `CLAUDE.md` passa a importar o `AGENTS.md`, e as declarações de copy passam a ser ancoradas pelo
|
|
13
|
+
conteúdo, não pela linha. A seção **Breaking** traz a única migração obrigatória, que atinge só quem
|
|
14
|
+
mantém instruções no `CLAUDE.md` que não estão no `AGENTS.md`.
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
|
|
18
|
+
`opus setup` mantém o bloco do Opus só no `AGENTS.md` e grava o `CLAUDE.md` como a linha
|
|
19
|
+
`@AGENTS.md`, que importa as instruções para o Claude Code (ADR 0061, no mesmo formato da ADR 0006 da
|
|
20
|
+
Base). Na conversão, o `setup` remove do `CLAUDE.md` o bloco do Opus e descarta o texto do projeto
|
|
21
|
+
que repete o do `AGENTS.md`. Texto que só existe no `CLAUDE.md` é preservado, e o `setup` e o
|
|
22
|
+
`opus pre-push materialization` reprovam até que ele vá para o `AGENTS.md`. Blocos de outros
|
|
23
|
+
pacotes ficam depois do import até que o pacote dono os remova: com uma Base anterior à ADR 0006, o
|
|
24
|
+
`CLAUDE.md` fica com o import e o bloco da Base.
|
|
25
|
+
|
|
26
|
+
Migre movendo para o `AGENTS.md` as instruções que estão só no `CLAUDE.md` e rodando `opus setup`.
|
|
27
|
+
|
|
28
|
+
### Âncora de copy pelo conteúdo
|
|
29
|
+
|
|
30
|
+
- **`copy.dynamic` por trecho.** A declaração seleciona o diagnóstico por `source`, `field` e
|
|
31
|
+
`expression`, o trecho de código que o extrator não resolveu, e continua válida quando linhas
|
|
32
|
+
entram ou saem acima dela. Espaços e quebras de linha do trecho são normalizados e a vírgula
|
|
33
|
+
final antes de `)`, `]` ou `}` é ignorada, para a âncora sobreviver à reformatação. A declaração
|
|
34
|
+
vale para toda ocorrência do mesmo trecho no mesmo campo do arquivo, e `dynamicSurfaces` registra
|
|
35
|
+
as linhas atuais de cada uma. O diagnóstico de conteúdo traz o `field` e a `expression` prontos
|
|
36
|
+
para a declaração.
|
|
37
|
+
- **`copy.exemptions` e `copy.transforms` por texto.** Sem `line`, `source`, `role` e `text`
|
|
38
|
+
selecionam toda ocorrência desse texto com esse papel no arquivo.
|
|
39
|
+
- **Seletores por linha continuam aceitos.** Eles selecionam exatamente um alvo, são aplicados
|
|
40
|
+
antes dos seletores por conteúdo e geram um aviso no `opus copy`. `opus copy --reanchor` converte
|
|
41
|
+
o `base.json` agrupando as declarações por arquivo e campo, ou por arquivo, papel e texto. Um
|
|
42
|
+
grupo só é convertido quando tem uma declaração para cada alvo, para que a âncora por conteúdo não
|
|
43
|
+
cubra um alvo que ninguém declarou; dentro dele, declarações e alvos são pareados em ordem de
|
|
44
|
+
linha, e as que mudaram de linha são listadas para revisão. Declarações idênticas se fundem, e
|
|
45
|
+
duas com motivos diferentes para o mesmo conteúdo continuam por linha, já corrigida. Os seletores
|
|
46
|
+
gravados passam na extração do `opus copy`; a política da Base não é avaliada, então rode
|
|
47
|
+
`opus copy --check` e `base copy check` depois e revise o diff.
|
|
48
|
+
- **`dynamicSurfaces` no inventário.** Uma declaração por conteúdo aparece com `expression` e
|
|
49
|
+
`lines`, as linhas atuais de cada ocorrência, no lugar de `line`. Uma lista `lines` que cresce no
|
|
50
|
+
diff indica que o trecho passou a cobrir uma ocorrência nova.
|
|
51
|
+
- **Erros que dizem onde.** Cada seletor que deixa de encontrar seu alvo aparece na mesma execução,
|
|
52
|
+
com o arquivo, o campo, o trecho e os diagnósticos daquele campo ainda sem declaração, ou com o
|
|
53
|
+
aviso de que o arquivo não existe mais no escopo.
|
|
54
|
+
|
|
55
|
+
### Instruções, documentação e changelog
|
|
56
|
+
|
|
57
|
+
- A instrução gerenciada do Opus passa a dizer que o inventário de copy guarda o hash de todo
|
|
58
|
+
arquivo JS/TS do escopo, inclusive scripts sem copy como `scripts/*.mjs`, e que alterar qualquer
|
|
59
|
+
um deles também exige `opus copy`.
|
|
60
|
+
- Os exemplos da documentação usam identificadores, nomes de action, domínios, eventos e permissões
|
|
61
|
+
em inglês (`ledger.close`, `ledger:close`, `invoice.issued`); rótulos, mensagens e comentários
|
|
62
|
+
continuam em pt-BR. O padrão editorial e a lista de verificação da skill `maintain-opus-docs`
|
|
63
|
+
passam a exigir isso, e um teste do repositório reprova identificadores com palavra em português.
|
|
64
|
+
- Entradas do changelog que já estão na `origin/main` do Opus não mudam mais: uma correção de
|
|
65
|
+
informação publicada entra como entrada nova. O pre-push e o `pnpm release` do repositório
|
|
66
|
+
reprovam a alteração.
|
|
67
|
+
|
|
68
|
+
## 28.0.0 — 2026-10-13
|
|
69
|
+
|
|
70
|
+
### Breaking
|
|
71
|
+
|
|
72
|
+
Page passa a usar `surfaceLayout="sectioned"` por padrão; `continuous` remove os divisores.
|
|
73
|
+
`className` de Page configura a moldura externa e o de PageBody, o viewport. Migre ajustes
|
|
74
|
+
internos de largura e alinhamento para `contentClassName`, disponível nos dois.
|
|
75
|
+
PageShell preserva rolagem de contingência para fallbacks; wrappers de rota devem propagar
|
|
76
|
+
altura para manter o header da Page fixo. Presentation fora do shell mantém largura total.
|
|
77
|
+
TabsPanel com `forceMount` preserva a montagem, mas oculta o painel inativo.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
A composição e a escala das superfícies mudam. Migre os consumidores conforme a ADR 0060 e remova os recuos locais substituídos pelo body.
|
|
81
|
+
|
|
82
|
+
DrawerTitle e DialogTitle na intro acompanham a escala do PageTitle; o header compacto
|
|
83
|
+
preserva seu título menor.
|
|
84
|
+
|
|
85
|
+
Surface, Page, Dialog, Drawer e Tabs compartilham ritmo vertical de 1rem. Superfícies delimitadas
|
|
86
|
+
fecham com padding inferior; corpos sem moldura ficam alinhados ao hospedeiro. Tabs framed
|
|
87
|
+
fornece os recuos do painel. Remova overrides locais que reconstruíam esses espaçamentos.
|
|
88
|
+
|
|
89
|
+
IconCatalogProvider permite catálogo assíncrono e renderização de assets sob demanda no IconPicker,
|
|
90
|
+
com busca no índice completo, grade de oito colunas com scroll infinito e tentativa após erro.
|
|
91
|
+
|
|
92
|
+
IconPicker apresenta uma grade sem nomes visíveis, com nomes acessíveis e busca por nomes e sinônimos
|
|
93
|
+
em português. O scroll do popover não se propaga ao modal. `searchTerms` personaliza a busca
|
|
94
|
+
sem modificar identificadores salvos. A paleta inclui Carro, pesquisável também por veículo e automóvel.
|
|
95
|
+
|
|
96
|
+
Toggle `outline` volta a distinguir o preenchimento ativo da borda, com fundo semitransparente
|
|
97
|
+
e preservação do formato conectado.
|
|
98
|
+
|
|
99
|
+
ActionList oferece `viewControls` para controles específicos da visualização ativa,
|
|
100
|
+
imediatamente após seu seletor. As configurações da lista aparecem antes dos seletores.
|
|
101
|
+
|
|
102
|
+
### Escala dos controles e composição
|
|
103
|
+
|
|
104
|
+
Corrigida a altura de `icon-sm`: 2rem, igual a `sm`, preservando o glifo de 0,875rem.
|
|
105
|
+
PaginationLink respeita o tamanho textual declarado; números, setas e elipse do pager
|
|
106
|
+
de ActionList usam 2rem. Remova compensações locais que fixavam esses controles em 1,75rem.
|
|
107
|
+
|
|
108
|
+
Page, Dialog, Drawer e Surface delimitada compartilham header com padding de 1rem e
|
|
109
|
+
linha mínima de 2rem. Ações de header usam `sm`/`icon-sm`, inclusive criação de coleções
|
|
110
|
+
e comandos de Presentation. Rodapés mantêm a escala anterior. Consumidores devem remover
|
|
111
|
+
compensações de altura/padding e declarar a escala compacta nos botões das barras.
|
|
112
|
+
|
|
113
|
+
Presentation em página passa a delegar padding e rolagem ao PageBody, evitando conteúdo
|
|
114
|
+
recortado e recuos duplicados. Ações de headers compactos sem navegação permanecem no
|
|
115
|
+
extremo oposto ao título.
|
|
116
|
+
|
|
117
|
+
Page oferece `PageIntro` e `PageIntroActions`, seguindo a separação entre controles da superfície
|
|
118
|
+
e conteúdo já disponível em Dialog e Drawer. A forma curta passa a colocar título e orientação
|
|
119
|
+
na intro, mantendo ações no header. Headers sem controles não reservam espaço. A forma explícita
|
|
120
|
+
compacta continua válida. A mudança visual exige migração coordenada dos consumidores (ADR 0060).
|
|
121
|
+
Page mantém header e footer fora da rolagem; o body rola em flow e repassa a altura à lista em fill.
|
|
122
|
+
|
|
10
123
|
## 27.1.0 — 2026-10-13
|
|
11
124
|
|
|
12
125
|
Portais documentais podem oferecer navegação recolhível em telas estreitas com
|
package/bin/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { cmdGen, helpGen } from './lib/gen.mjs'
|
|
|
15
15
|
import { cmdDb } from './lib/db.mjs'
|
|
16
16
|
import { scanDir, emptyScanVerdict } from './lib/check.mjs'
|
|
17
17
|
import { log } from './lib/cli-shared.mjs'
|
|
18
|
-
import { checkCopyInventory, writeCopyInventory } from './lib/copy.mjs'
|
|
18
|
+
import { checkCopyInventory, reanchorCopySelectors, writeCopyInventory } from './lib/copy.mjs'
|
|
19
19
|
import { createMonorepo, createProject } from './lib/create.mjs'
|
|
20
20
|
import { initProject, repoRootOf, setupUiFoundation } from './lib/init.mjs'
|
|
21
21
|
import { introspect } from './lib/introspect.mjs'
|
|
@@ -63,9 +63,9 @@ async function cmdCheck(dir) {
|
|
|
63
63
|
log('error', `✗ opus check: nenhuma action encontrada em ${rel} (${files} arquivos .ts); ${verdict.message}`)
|
|
64
64
|
process.exit(1)
|
|
65
65
|
}
|
|
66
|
-
const
|
|
66
|
+
const contractSuffix = contracts > 0 ? ` (${contracts} contrato(s))` : ''
|
|
67
67
|
if (findings.length === 0) {
|
|
68
|
-
log('success', `✓ opus check: ${actions} action(s)${
|
|
68
|
+
log('success', `✓ opus check: ${actions} action(s)${contractSuffix}; nenhuma violação encontrada.`)
|
|
69
69
|
return
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -97,10 +97,28 @@ async function cmdMaterializationCheck() {
|
|
|
97
97
|
async function cmdCopy(dir, flags) {
|
|
98
98
|
const selected = path.resolve(process.cwd(), dir ?? '.')
|
|
99
99
|
const root = (await repoRootOf(selected)) ?? selected
|
|
100
|
+
if (flags.reanchor) {
|
|
101
|
+
if (flags.check) {
|
|
102
|
+
log('error', '✗ opus copy: --reanchor grava o base.json e não combina com --check.')
|
|
103
|
+
process.exitCode = 1
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (!reportReanchor(reanchorCopySelectors(root))) {
|
|
107
|
+
process.exitCode = 1
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
}
|
|
100
111
|
const result = flags.check ? checkCopyInventory(root) : writeCopyInventory(root)
|
|
101
112
|
for (const diagnostic of result.diagnostics) {
|
|
102
113
|
log('error', `✗ opus copy: ${diagnostic.source}:${diagnostic.line} — ${diagnostic.message}`)
|
|
103
114
|
}
|
|
115
|
+
if (result.legacySelectors > 0) {
|
|
116
|
+
log(
|
|
117
|
+
'warn',
|
|
118
|
+
`! opus copy: ${result.legacySelectors} seletor(es) em base.json ainda usam \`line\`, que quebra a cada edição acima dele; ` +
|
|
119
|
+
'rode `opus copy --reanchor` para ancorá-los pelo conteúdo.',
|
|
120
|
+
)
|
|
121
|
+
}
|
|
104
122
|
if (!result.ok) {
|
|
105
123
|
if (result.reason === 'missing') {
|
|
106
124
|
log('error', `✗ opus copy: inventário ausente em ${path.relative(root, result.path)}; rode \`opus copy\` e versione o resultado.`)
|
|
@@ -121,6 +139,40 @@ async function cmdCopy(dir, flags) {
|
|
|
121
139
|
else log('success', `✓ opus copy: ${location} já estava atualizado (${summary}).`)
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
function reportReanchor(result) {
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
for (const diagnostic of result.diagnostics) {
|
|
145
|
+
log('error', `✗ opus copy --reanchor: ${diagnostic.source}:${diagnostic.line} — ${diagnostic.message}`)
|
|
146
|
+
}
|
|
147
|
+
log('error', '✗ opus copy --reanchor: resolva os problemas de leitura acima antes de reancorar; base.json não foi alterado.')
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
const { converted, realigned, relined, merged, unresolved } = result.report
|
|
151
|
+
const anchor = (item) => (item.anchor.expression === undefined ? `"${item.anchor.text}"` : `\`${item.anchor.expression}\``)
|
|
152
|
+
if (converted.length > 0) log('success', `✓ opus copy --reanchor: ${converted.length} seletor(es) passaram a ser ancorados pelo conteúdo.`)
|
|
153
|
+
// copy.dynamic identifica o alvo pelo campo; exceções e transformações, pelo papel do texto.
|
|
154
|
+
const target = (item) => (item.list === 'dynamic' ? `\`${item.selector.field}\`` : `(${item.selector.role})`)
|
|
155
|
+
if (realigned.length > 0) {
|
|
156
|
+
log('warn', `! opus copy --reanchor: ${realigned.length} âncora(s) quebradas pela mudança de linha foram convertidas; revise cada uma no diff:`)
|
|
157
|
+
for (const item of realigned) {
|
|
158
|
+
console.log(` copy.${item.list}[${item.index}] ${item.selector.source} ${target(item)} linha ${item.from} → ${item.to}: ${anchor(item)}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (relined.length > 0) {
|
|
162
|
+
log('warn', `! opus copy --reanchor: ${relined.length} seletor(es) continuam por linha, com a linha corrigida; revise cada um no diff:`)
|
|
163
|
+
for (const item of relined) {
|
|
164
|
+
console.log(` copy.${item.list}[${item.index}] ${item.selector.source} linha ${item.from} → ${item.to}: ${item.reason}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (merged.length > 0) log('success', `✓ opus copy --reanchor: ${merged.length} declaração(ões) repetida(s) fundida(s) com a idêntica.`)
|
|
168
|
+
if (unresolved.length > 0) {
|
|
169
|
+
log('warn', `! opus copy --reanchor: ${unresolved.length} seletor(es) continuam por linha:`)
|
|
170
|
+
for (const item of unresolved) console.log(` copy.${item.list}[${item.index}]: ${item.reason}`)
|
|
171
|
+
}
|
|
172
|
+
if (!result.changed) log('success', '✓ opus copy --reanchor: nada a converter em base.json.')
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
124
176
|
async function cmdCreate(dir, flags) {
|
|
125
177
|
if (!dir) {
|
|
126
178
|
log('error', 'Uso: opus create <dir> — o nome do diretório vira o nome do app (--monorepo: a raiz do workspace).')
|
|
@@ -273,6 +325,10 @@ function parseArgs(argv) {
|
|
|
273
325
|
flags.json = true
|
|
274
326
|
continue
|
|
275
327
|
}
|
|
328
|
+
if (arg === '--reanchor') {
|
|
329
|
+
flags.reanchor = true
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
276
332
|
if (arg === '--monorepo') {
|
|
277
333
|
flags.monorepo = true
|
|
278
334
|
continue
|
|
@@ -334,6 +390,7 @@ Flags
|
|
|
334
390
|
--help, -h Ajuda do comando.
|
|
335
391
|
--check (copy) Compara sem escrever; falha se o inventário estiver ausente ou
|
|
336
392
|
desatualizado.
|
|
393
|
+
--reanchor (copy) Ancora pelo conteúdo os seletores do base.json que usam linha.
|
|
337
394
|
--json (introspect, seed) Saída estruturada em JSON.
|
|
338
395
|
--monorepo (create) Cria a raiz do workspace em vez de um app.
|
|
339
396
|
--config <path> (gen, db, seed) Caminho do opus.config.ts. Default: ./opus.config.ts.
|
|
@@ -483,6 +540,14 @@ comprova que o extrator executou sobre uma fonte.
|
|
|
483
540
|
|
|
484
541
|
Flags:
|
|
485
542
|
--check Compara sem escrever; falha em ausência, drift ou copy dinâmica
|
|
543
|
+
sem declaração.
|
|
544
|
+
--reanchor Converte os seletores de copy.dynamic, copy.exemptions e
|
|
545
|
+
copy.transforms que ainda usam \`line\` para seletores pelo
|
|
546
|
+
conteúdo, grava o base.json e regenera o inventário. Só
|
|
547
|
+
converte quando há uma declaração para cada alvo, para não
|
|
548
|
+
ampliar nenhuma; as que mudaram de linha são listadas para
|
|
549
|
+
revisão, e o que não dá para converter com segurança fica
|
|
550
|
+
como está. Depois, rode base copy check e revise o diff.
|
|
486
551
|
`)
|
|
487
552
|
return
|
|
488
553
|
}
|
package/bin/lib/check.mjs
CHANGED
|
@@ -93,7 +93,11 @@ const KINDS = new Set(["simple", "form", "list", "view"]);
|
|
|
93
93
|
const ASSISTANT_RESOURCE_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
94
94
|
const ASSISTANT_SKILL_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
95
95
|
const ASSISTANT_CONTEXT_MIN = 1000;
|
|
96
|
-
const ASSISTANT_RESERVED_SEGMENTS = new Set([
|
|
96
|
+
const ASSISTANT_RESERVED_SEGMENTS = new Set([
|
|
97
|
+
"__proto__",
|
|
98
|
+
"constructor",
|
|
99
|
+
"prototype",
|
|
100
|
+
]);
|
|
97
101
|
const ASSISTANT_CONTEXT_MAX = 16000;
|
|
98
102
|
const ASSISTANT_REQUIRED = {
|
|
99
103
|
label: "`label`, o nome do assistente para pessoas",
|
|
@@ -150,9 +154,11 @@ const UI_SURFACE_PAIRS = new Map([
|
|
|
150
154
|
const UI_STRUCTURAL_PARENTS = new Map([
|
|
151
155
|
["PageHeader", new Set(["Page"])],
|
|
152
156
|
["PageBody", new Set(["Page"])],
|
|
157
|
+
["PageIntro", new Set(["PageBody"])],
|
|
158
|
+
["PageIntroActions", new Set(["PageIntro"])],
|
|
153
159
|
["PageFooter", new Set(["Page"])],
|
|
154
|
-
["PageTitle", new Set(["PageHeader"])],
|
|
155
|
-
["PageDescription", new Set(["PageHeader"])],
|
|
160
|
+
["PageTitle", new Set(["PageHeader", "PageIntro"])],
|
|
161
|
+
["PageDescription", new Set(["PageHeader", "PageIntro"])],
|
|
156
162
|
["PageBack", new Set(["PageHeader"])],
|
|
157
163
|
["PageNavigation", new Set(["PageHeader"])],
|
|
158
164
|
["PageActions", new Set(["PageHeader"])],
|
|
@@ -311,7 +317,6 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
311
317
|
["variant", "o cabeçalho da página tem uma única apresentação rolável"],
|
|
312
318
|
]),
|
|
313
319
|
],
|
|
314
|
-
|
|
315
320
|
]);
|
|
316
321
|
|
|
317
322
|
const UI_REMOVED_COMPONENTS = new Map([
|
|
@@ -323,7 +328,6 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
323
328
|
["PaneTitle", "use `SurfaceTitle` dentro de `SurfaceHeader`"],
|
|
324
329
|
["PaneDescription", "use `SurfaceDescription` dentro de `SurfaceHeader`"],
|
|
325
330
|
["PaneActions", "use `SurfaceActions` dentro de `SurfaceHeader`"],
|
|
326
|
-
["PageIntro", "use `PageHeader` como a única introdução estrutural"],
|
|
327
331
|
// Content e Card deram lugar a Surface (ADR 0045).
|
|
328
332
|
[
|
|
329
333
|
"Content",
|
|
@@ -356,6 +360,14 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
356
360
|
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
357
361
|
|
|
358
362
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
363
|
+
[
|
|
364
|
+
"PageIntro",
|
|
365
|
+
{
|
|
366
|
+
title: "PageTitle",
|
|
367
|
+
optional: new Set(["PageIntroActions", "PageDescription"]),
|
|
368
|
+
shorthand: new Set(),
|
|
369
|
+
},
|
|
370
|
+
],
|
|
359
371
|
[
|
|
360
372
|
"DialogIntro",
|
|
361
373
|
{
|
|
@@ -376,7 +388,12 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
376
388
|
"PageHeader",
|
|
377
389
|
{
|
|
378
390
|
title: "PageTitle",
|
|
379
|
-
optional: new Set([
|
|
391
|
+
optional: new Set([
|
|
392
|
+
"PageBack",
|
|
393
|
+
"PageNavigation",
|
|
394
|
+
"PageActions",
|
|
395
|
+
"PageDescription",
|
|
396
|
+
]),
|
|
380
397
|
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
381
398
|
shorthand: new Set(),
|
|
382
399
|
titleRequired: false,
|
|
@@ -568,7 +585,8 @@ export function parseRetention(fileName, sourceText) {
|
|
|
568
585
|
if (item === undefined) return undefined;
|
|
569
586
|
if (!ts.isPropertyAssignment(item)) return null;
|
|
570
587
|
const init = item.initializer;
|
|
571
|
-
return ts.isStringLiteral(init) ||
|
|
588
|
+
return ts.isStringLiteral(init) ||
|
|
589
|
+
ts.isNoSubstitutionTemplateLiteral(init)
|
|
572
590
|
? init.text
|
|
573
591
|
: null;
|
|
574
592
|
};
|
|
@@ -797,7 +815,9 @@ function checkRetention(retention) {
|
|
|
797
815
|
if (batch !== null && (!Number.isInteger(batch) || batch < 1))
|
|
798
816
|
at("`batchSize` precisa ser um inteiro positivo.");
|
|
799
817
|
else if (batch !== null && batch > MAX_PURGE_BATCH_SIZE)
|
|
800
|
-
at(
|
|
818
|
+
at(
|
|
819
|
+
`\`batchSize\` aceita no máximo ${MAX_PURGE_BATCH_SIZE} linhas por lote.`,
|
|
820
|
+
);
|
|
801
821
|
const inDomain =
|
|
802
822
|
entity.inlineInDomain ||
|
|
803
823
|
(entity.ident !== null && entityRefs.has(entity.ident));
|
|
@@ -813,7 +833,9 @@ function checkRetention(retention) {
|
|
|
813
833
|
for (const purge of parsed.purges) {
|
|
814
834
|
const subject = `bindPurge(${purge.entityRef ?? "?"})`;
|
|
815
835
|
const entity =
|
|
816
|
-
purge.entityRef === null
|
|
836
|
+
purge.entityRef === null
|
|
837
|
+
? undefined
|
|
838
|
+
: entitiesByIdent.get(purge.entityRef);
|
|
817
839
|
if (
|
|
818
840
|
entity !== undefined &&
|
|
819
841
|
entity !== "ambiguous" &&
|
|
@@ -982,15 +1004,20 @@ export function parseActions(fileName, sourceText) {
|
|
|
982
1004
|
/** `ai` literal → "enabled" | "disabled"; ausente → null; não literal → "unknown". */
|
|
983
1005
|
function aiState(obj) {
|
|
984
1006
|
const p = prop(obj, "ai");
|
|
985
|
-
if (!p)
|
|
1007
|
+
if (!p)
|
|
1008
|
+
return obj.properties.some((pr) => pr.name?.getText(sf) === "ai")
|
|
1009
|
+
? "unknown"
|
|
1010
|
+
: null;
|
|
986
1011
|
const value = p.initializer;
|
|
987
1012
|
if (value.kind === ts.SyntaxKind.TrueKeyword) return "enabled";
|
|
988
1013
|
if (value.kind === ts.SyntaxKind.FalseKeyword) return "disabled";
|
|
989
1014
|
if (ts.isObjectLiteralExpression(value)) {
|
|
990
1015
|
const enabled = prop(value, "enabled");
|
|
991
1016
|
if (!enabled) return "enabled";
|
|
992
|
-
if (enabled.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
993
|
-
|
|
1017
|
+
if (enabled.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
1018
|
+
return "disabled";
|
|
1019
|
+
if (enabled.initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
1020
|
+
return "enabled";
|
|
994
1021
|
}
|
|
995
1022
|
return "unknown";
|
|
996
1023
|
}
|
|
@@ -1007,14 +1034,17 @@ export function parseActions(fileName, sourceText) {
|
|
|
1007
1034
|
if (!found) return { present: objKeys(value).includes(key), text: null };
|
|
1008
1035
|
return {
|
|
1009
1036
|
present: true,
|
|
1010
|
-
text: ts.isStringLiteralLike(found.initializer)
|
|
1037
|
+
text: ts.isStringLiteralLike(found.initializer)
|
|
1038
|
+
? found.initializer.text
|
|
1039
|
+
: null,
|
|
1011
1040
|
};
|
|
1012
1041
|
};
|
|
1013
1042
|
const limit = prop(value, "maxContextChars");
|
|
1014
1043
|
let maxContextChars = null;
|
|
1015
1044
|
if (limit) {
|
|
1016
1045
|
const node = limit.initializer;
|
|
1017
|
-
if (ts.isNumericLiteral(node))
|
|
1046
|
+
if (ts.isNumericLiteral(node))
|
|
1047
|
+
maxContextChars = Number(node.text.replace(/_/g, ""));
|
|
1018
1048
|
else if (
|
|
1019
1049
|
ts.isPrefixUnaryExpression(node) &&
|
|
1020
1050
|
node.operator === ts.SyntaxKind.MinusToken &&
|
|
@@ -1025,7 +1055,9 @@ export function parseActions(fileName, sourceText) {
|
|
|
1025
1055
|
const contextNode = prop(value, "context");
|
|
1026
1056
|
const contextPaths =
|
|
1027
1057
|
contextNode && ts.isArrayLiteralExpression(contextNode.initializer)
|
|
1028
|
-
? contextNode.initializer.elements
|
|
1058
|
+
? contextNode.initializer.elements
|
|
1059
|
+
.filter(ts.isStringLiteralLike)
|
|
1060
|
+
.map((element) => element.text)
|
|
1029
1061
|
: [];
|
|
1030
1062
|
return {
|
|
1031
1063
|
readable: true,
|
|
@@ -1110,17 +1142,27 @@ function assistantMessages(a) {
|
|
|
1110
1142
|
if (!assistant.readable) return out;
|
|
1111
1143
|
const { resource, skill } = assistant;
|
|
1112
1144
|
if (!resource.present)
|
|
1113
|
-
out.push(
|
|
1145
|
+
out.push(
|
|
1146
|
+
"O assistente precisa declarar `resource`, o tipo de recurso de que a action é dona.",
|
|
1147
|
+
);
|
|
1114
1148
|
else if (resource.text !== null && !ASSISTANT_RESOURCE_RE.test(resource.text))
|
|
1115
|
-
out.push(
|
|
1149
|
+
out.push(
|
|
1150
|
+
`\`resource: "${resource.text}"\` precisa estar em camelCase, como \`customer\` ou \`aftersaleOpportunity\`.`,
|
|
1151
|
+
);
|
|
1116
1152
|
if (!skill.present)
|
|
1117
|
-
out.push(
|
|
1153
|
+
out.push(
|
|
1154
|
+
"O assistente precisa declarar `skill`, a chave do procedimento que a IA segue.",
|
|
1155
|
+
);
|
|
1118
1156
|
else if (skill.text !== null && !ASSISTANT_SKILL_RE.test(skill.text))
|
|
1119
|
-
out.push(
|
|
1157
|
+
out.push(
|
|
1158
|
+
`\`skill: "${skill.text}"\` precisa estar em minúsculas com hífen, como \`review-customer\`.`,
|
|
1159
|
+
);
|
|
1120
1160
|
const reserved = [assistant.title.text, ...assistant.contextPaths].filter(
|
|
1121
1161
|
(path) =>
|
|
1122
1162
|
typeof path === "string" &&
|
|
1123
|
-
path
|
|
1163
|
+
path
|
|
1164
|
+
.split(".")
|
|
1165
|
+
.some((segment) => ASSISTANT_RESERVED_SEGMENTS.has(segment)),
|
|
1124
1166
|
);
|
|
1125
1167
|
if (reserved.length > 0)
|
|
1126
1168
|
out.push(
|
|
@@ -1129,12 +1171,16 @@ function assistantMessages(a) {
|
|
|
1129
1171
|
const limit = assistant.maxContextChars;
|
|
1130
1172
|
if (
|
|
1131
1173
|
limit !== null &&
|
|
1132
|
-
(!Number.isInteger(limit) ||
|
|
1174
|
+
(!Number.isInteger(limit) ||
|
|
1175
|
+
limit < ASSISTANT_CONTEXT_MIN ||
|
|
1176
|
+
limit > ASSISTANT_CONTEXT_MAX)
|
|
1133
1177
|
)
|
|
1134
1178
|
out.push(
|
|
1135
1179
|
`\`maxContextChars: ${limit}\` precisa ser um inteiro de 1.000 a 16.000; sem ele, o teto é de 4.000 caracteres.`,
|
|
1136
1180
|
);
|
|
1137
|
-
const missing = Object.keys(ASSISTANT_REQUIRED).filter(
|
|
1181
|
+
const missing = Object.keys(ASSISTANT_REQUIRED).filter(
|
|
1182
|
+
(key) => !assistant.keys.includes(key),
|
|
1183
|
+
);
|
|
1138
1184
|
if (missing.length > 0)
|
|
1139
1185
|
out.push(
|
|
1140
1186
|
`O assistente precisa declarar ${missing.map((key) => ASSISTANT_REQUIRED[key]).join("; ")}.`,
|
|
@@ -1402,7 +1448,9 @@ export function checkProject(sources) {
|
|
|
1402
1448
|
|
|
1403
1449
|
for (const [resource, owners] of assistantResources) {
|
|
1404
1450
|
if (owners.length < 2) continue;
|
|
1405
|
-
const names = owners.map(
|
|
1451
|
+
const names = owners.map(
|
|
1452
|
+
({ item }) => item.name ?? item.ident ?? "(sem name)",
|
|
1453
|
+
);
|
|
1406
1454
|
for (const { item, file } of owners) {
|
|
1407
1455
|
findings.push({
|
|
1408
1456
|
rule: "assistant-declaration",
|
|
@@ -1707,6 +1755,22 @@ function directStructuralChildren(node, imports) {
|
|
|
1707
1755
|
for (const child of node.children) {
|
|
1708
1756
|
if (ts.isJsxText(child) && child.text.trim() === "") continue;
|
|
1709
1757
|
if (ts.isJsxExpression(child) && child.expression === undefined) continue;
|
|
1758
|
+
if (
|
|
1759
|
+
ts.isJsxExpression(child) &&
|
|
1760
|
+
child.expression &&
|
|
1761
|
+
ts.isConditionalExpression(child.expression)
|
|
1762
|
+
) {
|
|
1763
|
+
const branches = [child.expression.whenTrue, child.expression.whenFalse];
|
|
1764
|
+
const slots = branches.map((branch) =>
|
|
1765
|
+
ts.isJsxElement(branch) || ts.isJsxSelfClosingElement(branch)
|
|
1766
|
+
? imports.get(jsxLocalName(branch))
|
|
1767
|
+
: undefined,
|
|
1768
|
+
);
|
|
1769
|
+
if (slots[0] !== undefined && slots[0] === slots[1]) {
|
|
1770
|
+
names.push(slots[0]);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1710
1774
|
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
1711
1775
|
const local = jsxLocalName(child);
|
|
1712
1776
|
const imported = local === null ? null : imports.get(local);
|
|
@@ -1957,6 +2021,7 @@ function countPageHeadingSources(page, imports) {
|
|
|
1957
2021
|
local !== null &&
|
|
1958
2022
|
((imports.get(local) === "Surface" &&
|
|
1959
2023
|
jsxNumericAttribute(node, "level") === 1) ||
|
|
2024
|
+
imports.get(local) === "PageIntro" ||
|
|
1960
2025
|
imports.get(local) === "PageState")
|
|
1961
2026
|
) {
|
|
1962
2027
|
if (imports.get(local) === "PageState") states += 1;
|
|
@@ -2095,11 +2160,55 @@ export function checkUiStructure(file, text) {
|
|
|
2095
2160
|
}
|
|
2096
2161
|
}
|
|
2097
2162
|
|
|
2098
|
-
if (
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2163
|
+
if (["PageBody", "DialogBody", "DrawerBody"].includes(component)) {
|
|
2164
|
+
const intro = component.replace("Body", "Intro");
|
|
2165
|
+
const visible = (children) =>
|
|
2166
|
+
children.flatMap((child) => {
|
|
2167
|
+
if (ts.isJsxText(child)) return child.text.trim() ? [child] : [];
|
|
2168
|
+
if (ts.isJsxFragment(child)) return visible(child.children);
|
|
2169
|
+
if (
|
|
2170
|
+
ts.isJsxExpression(child) &&
|
|
2171
|
+
(!child.expression ||
|
|
2172
|
+
[
|
|
2173
|
+
ts.SyntaxKind.NullKeyword,
|
|
2174
|
+
ts.SyntaxKind.FalseKeyword,
|
|
2175
|
+
ts.SyntaxKind.TrueKeyword,
|
|
2176
|
+
].includes(child.expression.kind))
|
|
2177
|
+
)
|
|
2178
|
+
return [];
|
|
2179
|
+
return [child];
|
|
2180
|
+
});
|
|
2181
|
+
const children = ts.isJsxElement(node) ? visible(node.children) : [];
|
|
2182
|
+
const indexes = children.flatMap((child, index) =>
|
|
2183
|
+
(ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) &&
|
|
2184
|
+
imports.get(jsxLocalName(child)) === intro
|
|
2185
|
+
? [index]
|
|
2186
|
+
: [],
|
|
2187
|
+
);
|
|
2188
|
+
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
2189
|
+
add(
|
|
2190
|
+
node,
|
|
2191
|
+
component,
|
|
2192
|
+
`${intro} deve ser o primeiro e único bloco introdutório de ${component}.`,
|
|
2193
|
+
"ui-structure-intro-order",
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
if (
|
|
2199
|
+
component !== null &&
|
|
2200
|
+
(UI_STRUCTURAL_BODIES.has(component) || component === "Page")
|
|
2201
|
+
) {
|
|
2202
|
+
const attributes =
|
|
2203
|
+
component === "Page"
|
|
2204
|
+
? ["contentClassName"]
|
|
2205
|
+
: component === "PageBody"
|
|
2206
|
+
? ["className", "contentClassName"]
|
|
2207
|
+
: ["className"];
|
|
2208
|
+
const overridden = attributes
|
|
2209
|
+
.flatMap((attribute) =>
|
|
2210
|
+
jsxStringAttributeValues(jsxOpening(node), attribute),
|
|
2211
|
+
)
|
|
2103
2212
|
.flatMap((value) => value.trim().split(/\s+/))
|
|
2104
2213
|
.filter((token) => UI_BODY_RHYTHM_CLASS.test(token));
|
|
2105
2214
|
if (overridden.length > 0) {
|
|
@@ -2686,8 +2795,8 @@ export async function scanDir(rootDir) {
|
|
|
2686
2795
|
for (const file of files) {
|
|
2687
2796
|
const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
|
|
2688
2797
|
if (
|
|
2689
|
-
![...ACTION_MARKERS, ...DATA_PRODUCT_MARKERS, ...RETENTION_MARKERS].some(
|
|
2690
|
-
text.includes(m),
|
|
2798
|
+
![...ACTION_MARKERS, ...DATA_PRODUCT_MARKERS, ...RETENTION_MARKERS].some(
|
|
2799
|
+
(m) => text.includes(m),
|
|
2691
2800
|
)
|
|
2692
2801
|
)
|
|
2693
2802
|
continue;
|