@softize/opus 11.1.1 → 12.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 +51 -1
- package/README.md +31 -1
- package/bin/cli.mjs +97 -27
- package/bin/lib/check.mjs +55 -43
- package/bin/lib/copy.mjs +2202 -0
- package/bin/lib/create.mjs +227 -39
- package/bin/lib/db-migrate-runner.mjs +9 -6
- package/bin/lib/db-project-path.mjs +20 -0
- package/bin/lib/db-scaffold-runner.mjs +23 -8
- package/bin/lib/db.mjs +6 -4
- package/bin/lib/gen.mjs +60 -29
- package/bin/lib/init.mjs +212 -56
- package/bin/lib/introspect.mjs +3 -2
- package/bin/lib/materialize.mjs +623 -97
- package/bin/lib/postinstall.mjs +6 -5
- package/bin/lib/validate-skill.mjs +502 -30
- package/docs/code-style.md +142 -7
- package/docs/consumer-upgrade-propagation.md +4 -3
- package/docs/releasing.md +28 -17
- package/package.json +6 -1
- package/registry/git/pre-push.d/00-opus-copy +14 -0
- package/registry/git/pre-push.d/opus +7 -21
- package/registry/git/run-opus-pre-push.mjs +141 -0
- package/registry/hooks/opus-check-on-stop.mjs +13 -31
- package/registry/instructions/opus.md +11 -5
- package/registry/skills/build-opus-ui/SKILL.md +5 -4
- package/registry/skills/create-opus-action/SKILL.md +4 -4
- package/registry/skills/implement-opus-change/SKILL.md +7 -5
- package/registry/skills/upgrade-opus/SKILL.md +8 -4
- package/registry/skills/upgrade-opus/references/upgrade-checklist.md +4 -1
- package/registry/templates/app/package.json +4 -0
- package/registry/templates/app/pnpm-workspace.yaml +3 -2
- package/registry/templates/app/src/domains/tasks/actions/list.ts +1 -1
- package/registry/templates/monorepo/pnpm-workspace.yaml +3 -1
- package/src/ui/docs/content/cli.md +8 -7
- package/src/ui/docs/content/communication.md +79 -126
- package/src/ui/docs/content/getting-started.md +29 -16
- package/registry/skills/write-product-communication/SKILL.md +0 -28
- package/registry/skills/write-product-communication/agents/openai.yaml +0 -4
- package/registry/templates/app/_npmrc +0 -1
- package/registry/templates/monorepo/_npmrc +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,57 @@ O que muda em cada versão — e, quando quebra, **o que fazer**. Regras de leit
|
|
|
4
4
|
minor = novidade compatível; major = breaking (a seção **Breaking** diz a migração).
|
|
5
5
|
Este arquivo viaja no pacote: num projeto, leia `node_modules/@softize/opus/CHANGELOG.md`.
|
|
6
6
|
Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
7
|
-
`manifest:check`) — eles apontam o que a
|
|
7
|
+
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
|
+
mudança cobra do seu código.
|
|
9
|
+
|
|
10
|
+
## 12.0.0 — 2026-08-21
|
|
11
|
+
|
|
12
|
+
**Copy declarativa passa a ter inventário semântico e gate reproduzível.** `opus copy`
|
|
13
|
+
projeta texto estático dos contratos e de uma allowlist explícita de componentes
|
|
14
|
+
`@softize/opus/ui` e subpaths para o protocolo v2 da `@softize/base`; `--check` confere
|
|
15
|
+
ausência e drift sem escrever. Factories e componentes são reconhecidos pela origem do
|
|
16
|
+
import, inclusive alias e namespace, sem confundir homônimo local. Spreads, duplicatas,
|
|
17
|
+
chaves computadas literais e escopo léxico seguem a semântica real quando estáticos; uma
|
|
18
|
+
incerteza relevante falha em vez de sumir do inventário. `aria-label` não mascara texto
|
|
19
|
+
visual opaco, e `Select`/`ActionTrigger` têm suas superfícies estáveis classificadas.
|
|
20
|
+
Fontes recebem SHA-256, a geração troca o arquivo atomicamente e não atravessa links simbólicos
|
|
21
|
+
no caminho do inventário. O manifesto `source-manifest-v1` inclui todo JS/TS efetivamente
|
|
22
|
+
analisado, mesmo sem entradas, e deriva as contagens de arquivos, fontes cobertas e textos;
|
|
23
|
+
zero arquivo falha em vez de simular uma execução vazia. Transformações e exceções usam
|
|
24
|
+
seletores explícitos em `base.json`; exceções v2 exigem `ruleId`, `kind`, `reason` e
|
|
25
|
+
`reference` auditável.
|
|
26
|
+
|
|
27
|
+
A Base 2.0 passa a ser a única dona da skill geral `write-product-communication`. O Opus não
|
|
28
|
+
mantém um segundo registry com o mesmo slug; sua página `communication.md` registra somente
|
|
29
|
+
vocabulário, papéis semânticos, cobertura e convenções específicas de componentes.
|
|
30
|
+
|
|
31
|
+
`opus setup` configura e gera `.base/copy-inventory.json`. O fragmento
|
|
32
|
+
`00-opus-copy` roda antes de `base` no dispatcher compartilhado, e projetos novos recebem
|
|
33
|
+
scripts `setup`/`copy`/`copy:check` e `@softize/base ^2.0.0`. `pnpm run setup`
|
|
34
|
+
materializa os dois fragmentos do pre-push, gerando o inventário Opus antes de a Base validar
|
|
35
|
+
freshness. O próprio repositório e a release do Opus materializam e validam a Base 2.0.
|
|
36
|
+
|
|
37
|
+
**Breaking — materialização e análise mais conservadoras.** A resolução dos hooks usa o workspace
|
|
38
|
+
declarado em `base.json`, e o
|
|
39
|
+
extrator não aceita como estáticas referências mutadas, escapadas nem condições JSX de tipo
|
|
40
|
+
incerto. Superfícies visuais consideram `style.textTransform`, herança, slots e conteúdo ARIA
|
|
41
|
+
opaco. Quando o texto não pode ser provado estaticamente, o diagnóstico exige copy estática ou
|
|
42
|
+
uma exceção auditável em vez de omitir a superfície.
|
|
43
|
+
|
|
44
|
+
As operações sobre o projeto passam a falhar de forma fechada quando encontram links
|
|
45
|
+
simbólicos, hardlinks mutáveis ou caminhos que mudam durante a leitura/escrita. Isso vale para
|
|
46
|
+
setup, inventário, scaffold, `add`, geração, comandos de banco e scans estáticos; assets internos
|
|
47
|
+
do pacote continuam sendo lidos da instalação. `opus gen --config/--output` e `opus db --config`
|
|
48
|
+
agora aceitam somente caminhos contidos no diretório do projeto em que o comando foi iniciado.
|
|
49
|
+
Um source ilegível ou inseguro deixa de ser pulado silenciosamente e reprova o comando.
|
|
50
|
+
|
|
51
|
+
**Migração:** instale `@softize/base 2.0.0`, rode `pnpm run setup`, revise e versione o
|
|
52
|
+
inventário gerado. Corrija os achados de `base copy check`; para conteúdo dinâmico, forneça
|
|
53
|
+
fallback estático ou documente a superfície fora da cobertura do extrator. O setup remove do
|
|
54
|
+
projeto a configuração legada de `registry.softize.com.br`; remova também a configuração
|
|
55
|
+
global antiga com `pnpm config delete @softize:registry --global`. Para gerar ou migrar conteúdo
|
|
56
|
+
fora do projeto, execute o comando a partir dessa outra raiz ou mova a saída explicitamente
|
|
57
|
+
depois que o gate terminar.
|
|
8
58
|
|
|
9
59
|
## 11.1.1 — 2026-08-21
|
|
10
60
|
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ src/
|
|
|
82
82
|
core/ schema/ server/ client/ ui/
|
|
83
83
|
data/ auth/ audit/ log/ queue/ events/ scheduler/ dsl/
|
|
84
84
|
registry/ # scaffolds + skills Opus — geração e conhecimento do SDK, não runtime
|
|
85
|
-
bin/ # CLI (opus setup/gen/check/introspect/mcp) + libs
|
|
85
|
+
bin/ # CLI (opus setup/gen/copy/check/introspect/mcp) + libs
|
|
86
86
|
docs/
|
|
87
87
|
protocol.md # contrato completo (16 seções)
|
|
88
88
|
data-layer.md · releasing.md · code-style.md
|
|
@@ -102,6 +102,36 @@ pnpm test # vitest run
|
|
|
102
102
|
pnpm test:cov # com coverage report
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## Qualidade de copy
|
|
106
|
+
|
|
107
|
+
O Opus extrai o papel semântico dos textos declarados nos contratos; a política universal e
|
|
108
|
+
seus fundamentos ficam na `@softize/base`. Uma allowlist pequena também cobre texto estático
|
|
109
|
+
de componentes importados diretamente de `@softize/opus/ui` ou seus subpaths. O inventário
|
|
110
|
+
derivado usa o protocolo v2, é versionado e conferido sem escrita nos gates:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# No projeto criado por `opus create` (o scaffold declara este script):
|
|
114
|
+
pnpm run setup
|
|
115
|
+
pnpm exec opus copy
|
|
116
|
+
pnpm exec opus copy --check
|
|
117
|
+
pnpm exec base copy check
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Neste repositório-fonte, que não declara um script `setup` na raiz, a materialização
|
|
121
|
+
equivalente usa os comandos reais e os cwd exigidos por cada protocolo:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
node packages/opus/bin/cli.mjs setup
|
|
125
|
+
pnpm --filter @softize/opus exec base setup
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
O manifesto lista com hash todo JS/TS submetido à análise, inclusive arquivos com zero entradas,
|
|
129
|
+
e reporta as contagens observáveis; ele não afirma cobertura total do repositório. O gate verde
|
|
130
|
+
não cobre JSX nativo nem wrappers locais; essas superfícies continuam na revisão editorial.
|
|
131
|
+
Texto de runtime que ocupa uma superfície Opus mapeada falha visivelmente, e `aria-label` não
|
|
132
|
+
mascara copy visual opaca. Detalhes e mapeamentos estão em
|
|
133
|
+
[docs/code-style.md](docs/code-style.md).
|
|
134
|
+
|
|
105
135
|
## Publicar
|
|
106
136
|
|
|
107
137
|
Publicado no npm público (`registry.npmjs.org`). `pnpm release
|
package/bin/cli.mjs
CHANGED
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
import { promises as fs } from 'node:fs'
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import {
|
|
19
|
+
canonicalProjectDirectory,
|
|
20
|
+
ensureProjectDirectory,
|
|
21
|
+
readProjectFile,
|
|
22
|
+
safeProjectPath,
|
|
23
|
+
writeProjectFileAtomically,
|
|
24
|
+
} from '@softize/base/project-path'
|
|
18
25
|
|
|
19
26
|
import { cmdGen, helpGen } from './lib/gen.mjs'
|
|
20
27
|
import { cmdDb } from './lib/db.mjs'
|
|
21
28
|
import { scanDir, emptyScanVerdict } from './lib/check.mjs'
|
|
29
|
+
import { checkCopyInventory, writeCopyInventory } from './lib/copy.mjs'
|
|
22
30
|
import { createMonorepo, createProject } from './lib/create.mjs'
|
|
23
31
|
import { initProject, repoRootOf, setupUiFoundation } from './lib/init.mjs'
|
|
24
32
|
import { introspect } from './lib/introspect.mjs'
|
|
@@ -54,13 +62,12 @@ async function fileExists(p) {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
async function loadComponentsJson(cwd) {
|
|
57
|
-
const candidate =
|
|
58
|
-
if (!
|
|
65
|
+
const candidate = readProjectFile(cwd, 'components.json', { allowMissing: true })
|
|
66
|
+
if (!candidate.exists) {
|
|
59
67
|
return null
|
|
60
68
|
}
|
|
61
69
|
try {
|
|
62
|
-
|
|
63
|
-
return JSON.parse(raw)
|
|
70
|
+
return JSON.parse(candidate.content)
|
|
64
71
|
} catch (err) {
|
|
65
72
|
log('warn', `components.json existe mas não é JSON válido: ${err.message}`)
|
|
66
73
|
return null
|
|
@@ -84,12 +91,12 @@ async function resolveDestination(cwd, name) {
|
|
|
84
91
|
'warn',
|
|
85
92
|
'Sem components.json. Usando default ./src/components/action/. Roda `npx shadcn init` se quiser configurar.',
|
|
86
93
|
)
|
|
87
|
-
return path.join(
|
|
94
|
+
return path.join('src/components/action', `${name}.tsx`)
|
|
88
95
|
}
|
|
89
96
|
const aliasComponents =
|
|
90
97
|
components.aliases?.components ?? '@/components'
|
|
91
98
|
const componentsRel = resolveAlias(aliasComponents, 'components')
|
|
92
|
-
return path.join(
|
|
99
|
+
return path.join('src', componentsRel, 'action', `${name}.tsx`)
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
async function listTemplates() {
|
|
@@ -140,26 +147,34 @@ async function cmdAdd(name, options) {
|
|
|
140
147
|
process.exit(1)
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
const cwd = process.cwd()
|
|
150
|
+
const cwd = canonicalProjectDirectory(process.cwd())
|
|
144
151
|
const src = path.join(REGISTRY_DIR, `${name}.tsx`)
|
|
145
152
|
const dest = await resolveDestination(cwd, name)
|
|
153
|
+
safeProjectPath(cwd, dest)
|
|
154
|
+
const current = readProjectFile(cwd, dest, { allowMissing: true })
|
|
146
155
|
|
|
147
|
-
if (
|
|
156
|
+
if (current.exists) {
|
|
148
157
|
if (options.force !== true) {
|
|
149
158
|
log(
|
|
150
159
|
'warn',
|
|
151
|
-
`Já existe em ${
|
|
160
|
+
`Já existe em ${dest} — passa --force pra sobrescrever.`,
|
|
152
161
|
)
|
|
153
162
|
process.exit(1)
|
|
154
163
|
}
|
|
155
|
-
log('warn', `Sobrescrevendo ${
|
|
164
|
+
log('warn', `Sobrescrevendo ${dest}`)
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
|
|
167
|
+
const parents = []
|
|
168
|
+
let parent = path.dirname(dest)
|
|
169
|
+
while (parent !== '.') {
|
|
170
|
+
parents.unshift(parent)
|
|
171
|
+
parent = path.dirname(parent)
|
|
172
|
+
}
|
|
173
|
+
for (const directory of parents) ensureProjectDirectory(cwd, directory)
|
|
159
174
|
const content = await fs.readFile(src, 'utf-8')
|
|
160
|
-
|
|
175
|
+
writeProjectFileAtomically(cwd, dest, content, { exists: current.exists, content: current.content })
|
|
161
176
|
|
|
162
|
-
log('success', `✓ Criado ${
|
|
177
|
+
log('success', `✓ Criado ${dest}`)
|
|
163
178
|
console.log(
|
|
164
179
|
'\n Edita à vontade — esse arquivo é seu agora. Próximas runs com --force\n sobrescrevem suas mudanças.\n',
|
|
165
180
|
)
|
|
@@ -219,6 +234,32 @@ async function cmdMaterializationCheck() {
|
|
|
219
234
|
log('success', '✓ opus check: artefatos materializados estão atualizados.')
|
|
220
235
|
}
|
|
221
236
|
|
|
237
|
+
async function cmdCopy(dir, flags) {
|
|
238
|
+
const selected = path.resolve(process.cwd(), dir ?? '.')
|
|
239
|
+
const root = (await repoRootOf(selected)) ?? selected
|
|
240
|
+
const result = flags.check ? checkCopyInventory(root) : writeCopyInventory(root)
|
|
241
|
+
for (const diagnostic of result.diagnostics) {
|
|
242
|
+
log('error', `✗ opus copy: ${diagnostic.source}:${diagnostic.line} — ${diagnostic.message}`)
|
|
243
|
+
}
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
if (result.reason === 'missing') {
|
|
246
|
+
log('error', `✗ opus copy: inventário ausente em ${path.relative(root, result.path)}; rode \`opus copy\` e versione o resultado.`)
|
|
247
|
+
} else if (result.reason === 'stale') {
|
|
248
|
+
log('error', `✗ opus copy: inventário desatualizado em ${path.relative(root, result.path)}; rode \`opus copy\` e revise o diff.`)
|
|
249
|
+
}
|
|
250
|
+
process.exitCode = 1
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
const location = path.relative(root, result.path) || '.'
|
|
254
|
+
const coverage = result.inventory.coverage
|
|
255
|
+
const summary =
|
|
256
|
+
`${coverage.analyzedFiles} arquivo(s) analisado(s), ` +
|
|
257
|
+
`${coverage.coveredSources} fonte(s) com copy, ${coverage.extractedEntries} texto(s)`
|
|
258
|
+
if (flags.check) log('success', `✓ opus copy: ${location} está atualizado (${summary}).`)
|
|
259
|
+
else if (result.changed) log('success', `✓ opus copy: ${location} gerado atomicamente (${summary}).`)
|
|
260
|
+
else log('success', `✓ opus copy: ${location} já estava atualizado (${summary}).`)
|
|
261
|
+
}
|
|
262
|
+
|
|
222
263
|
async function cmdCreate(dir, flags) {
|
|
223
264
|
if (!dir) {
|
|
224
265
|
log('error', 'Uso: opus create <dir> — o nome do diretório vira o nome do app (--monorepo: a raiz do workspace).')
|
|
@@ -229,8 +270,8 @@ async function cmdCreate(dir, flags) {
|
|
|
229
270
|
|
|
230
271
|
if (flags.monorepo) {
|
|
231
272
|
const r = await createMonorepo(REGISTRY_DIR, target, name)
|
|
232
|
-
log('success', `✓ opus create: raiz do monorepo "${name}" criada (
|
|
233
|
-
console.log(`\n ${r.files.length} arquivos (workspace apps/* + packages/*,
|
|
273
|
+
log('success', `✓ opus create: raiz do monorepo "${name}" criada (Opus v${r.version}).`)
|
|
274
|
+
console.log(`\n ${r.files.length} arquivos (workspace apps/* + packages/*, allowBuilds e quarentena coordenada).`)
|
|
234
275
|
console.log(`
|
|
235
276
|
Próximos passos:
|
|
236
277
|
cd ${dir}
|
|
@@ -245,7 +286,7 @@ async function cmdCreate(dir, flags) {
|
|
|
245
286
|
throw new Error(`setup incompleto: ${r.setup.materialization.errors.join(' | ')}`)
|
|
246
287
|
}
|
|
247
288
|
const modeLabel = r.mode === 'app' ? 'app no monorepo (workspace detectado)' : 'repo standalone'
|
|
248
|
-
log('success', `✓ opus create: app "${name}" criado (
|
|
289
|
+
log('success', `✓ opus create: app "${name}" criado (Opus v${r.version}, ${modeLabel}).`)
|
|
249
290
|
console.log(`\n ${r.files.length} arquivos do esqueleto + setup (${r.setup.created.join(', ')}).`)
|
|
250
291
|
const warnings = [...r.warnings, ...r.setup.warnings]
|
|
251
292
|
if (warnings.length) {
|
|
@@ -256,7 +297,8 @@ async function cmdCreate(dir, flags) {
|
|
|
256
297
|
console.log(`
|
|
257
298
|
Próximos passos (no monorepo):
|
|
258
299
|
pnpm install
|
|
259
|
-
pnpm --filter ${name}
|
|
300
|
+
pnpm --filter ${name} run setup
|
|
301
|
+
pnpm --filter ${name} test && pnpm --filter ${name} copy:check && pnpm --filter ${name} manifest
|
|
260
302
|
|
|
261
303
|
Opcional: cadastre o app em uma interface de desenvolvimento como o Maestro.
|
|
262
304
|
`)
|
|
@@ -264,9 +306,11 @@ async function cmdCreate(dir, flags) {
|
|
|
264
306
|
console.log(`
|
|
265
307
|
Próximos passos:
|
|
266
308
|
cd ${dir}
|
|
267
|
-
git init
|
|
309
|
+
git init
|
|
268
310
|
pnpm install
|
|
269
|
-
pnpm
|
|
311
|
+
pnpm run setup
|
|
312
|
+
pnpm test && pnpm exec opus check src && pnpm copy:check && pnpm manifest
|
|
313
|
+
git add -A && git commit -m "chore: esqueleto opus"
|
|
270
314
|
|
|
271
315
|
Opcional: cadastre o repo em uma interface de desenvolvimento como o Maestro.
|
|
272
316
|
`)
|
|
@@ -279,6 +323,7 @@ async function cmdInit() {
|
|
|
279
323
|
if (!r.materialization.ok) {
|
|
280
324
|
for (const error of r.materialization.errors) log('error', `✗ opus setup: ${error}`)
|
|
281
325
|
process.exitCode = 1
|
|
326
|
+
return
|
|
282
327
|
}
|
|
283
328
|
log('success', `✓ opus setup: app v${r.version} ${r.wasInitialized ? 'atualizado' : 'inicializado'}.`)
|
|
284
329
|
if (r.created.length) {
|
|
@@ -311,7 +356,7 @@ async function cmdInit() {
|
|
|
311
356
|
}
|
|
312
357
|
|
|
313
358
|
console.log(
|
|
314
|
-
'\n
|
|
359
|
+
'\n Por app: opus.json. No repo: base.json + skills/hooks/instruções específicos do Opus.\n' +
|
|
315
360
|
' A spec vive nas declarações (description → manifest). Gate: opus check.\n',
|
|
316
361
|
)
|
|
317
362
|
}
|
|
@@ -402,9 +447,10 @@ Comandos:
|
|
|
402
447
|
create <dir> Scaffolda um app opus-based canônico (vite+react+ui+domínio-exemplo);
|
|
403
448
|
detecta workspace (app em monorepo); --monorepo cria a RAIZ do workspace
|
|
404
449
|
setup Reconcilia opus.json e os artefatos Opus versionados para Claude/Codex
|
|
405
|
-
add <name> Copia template do
|
|
450
|
+
add <name> Copia template do catálogo empacotado pro projeto
|
|
406
451
|
list Lista templates disponíveis
|
|
407
452
|
gen Gera manifest/openapi/docs/stubs a partir do opus.config.ts
|
|
453
|
+
copy [dir] Gera o inventário semântico de copy dos contratos; --check só confere
|
|
408
454
|
check [dir] Valida as convenções das actions (régua de padrão; exit ≠ 0 se violar)
|
|
409
455
|
pre-push Gate Git: freshness dos artefatos + convenções das actions
|
|
410
456
|
db <verbo> Comandos de banco. v1: db check (drift-check entidade ↔ banco)
|
|
@@ -414,6 +460,7 @@ Comandos:
|
|
|
414
460
|
|
|
415
461
|
Flags:
|
|
416
462
|
--force, -f Sobrescreve arquivo existente
|
|
463
|
+
--check Não escreve; falha se o inventário de copy estiver ausente/desatualizado
|
|
417
464
|
--config <path> (gen) Caminho do opus.config.ts
|
|
418
465
|
--output <path> (gen) Pasta de saída
|
|
419
466
|
|
|
@@ -432,13 +479,13 @@ Exemplos:
|
|
|
432
479
|
@softize/opus create <dir>
|
|
433
480
|
|
|
434
481
|
Scaffolda um app opus-based CANÔNICO no diretório <dir> (recusa dir não-vazio).
|
|
435
|
-
O esqueleto versiona com
|
|
482
|
+
O esqueleto versiona com o Opus — nasce com os pré-requisitos plugados:
|
|
436
483
|
protocolo opus.config.ts + domínio-exemplo + manifest/scripts (+ teste verde)
|
|
437
484
|
UI vite + react + tailwind v4 CSS-first (tema + @source do Opus)
|
|
438
485
|
desenvolvimento dev server na porta injetada (PORT) + allowedHosts do preview
|
|
439
|
-
|
|
486
|
+
automação opus.json + skills/hooks/instruções específicos do Opus
|
|
440
487
|
|
|
441
|
-
Depois: git init && pnpm install && pnpm test — e cadastre repo/app no admin.
|
|
488
|
+
Depois: git init && pnpm install && pnpm run setup && pnpm test — e cadastre repo/app no admin.
|
|
442
489
|
`)
|
|
443
490
|
return
|
|
444
491
|
}
|
|
@@ -453,14 +500,16 @@ Depois: git init && pnpm install && pnpm test — e cadastre repo/app no admin.
|
|
|
453
500
|
|
|
454
501
|
Setup idempotente de um projeto Opus:
|
|
455
502
|
opus.json marcador per-app com a versão do SDK
|
|
456
|
-
base.json
|
|
457
|
-
agentes skills
|
|
503
|
+
base.json configuração compartilhada e versão aplicada do Opus
|
|
504
|
+
agentes skills, hook, instruções e pre-push específicos do SDK
|
|
458
505
|
|
|
459
506
|
Fundação de UI (só apps web — preset Tailwind + tema + flags do Opus):
|
|
460
507
|
tailwind.config criado se faltar; senão avisa pra estender o preset
|
|
461
508
|
tema/tsconfig/dep — avisa o que falta plugar (não edita seus arquivos: sem clobber)
|
|
462
509
|
|
|
463
|
-
|
|
510
|
+
Este comando materializa somente os artefatos Opus. O template encadeia
|
|
511
|
+
\`opus setup && base setup\`: a Base 2.0 é obrigatória quando o gate universal de copy
|
|
512
|
+
está habilitado; Maestro continua opcional.
|
|
464
513
|
`)
|
|
465
514
|
return
|
|
466
515
|
}
|
|
@@ -524,6 +573,27 @@ passa verde). Default dir: cwd.
|
|
|
524
573
|
return
|
|
525
574
|
}
|
|
526
575
|
|
|
576
|
+
if (command === 'copy') {
|
|
577
|
+
if (flags.help) {
|
|
578
|
+
console.log(`
|
|
579
|
+
@softize/opus copy [dir]
|
|
580
|
+
|
|
581
|
+
Projeta textos de \`defineAction\`/\`defineContract\` no protocolo v2 da
|
|
582
|
+
@softize/base. O caminho vem de \`base.json.copy.inventory\`; o default é
|
|
583
|
+
\`.base/copy-inventory.json\` na raiz Git. A geração é atômica e o arquivo
|
|
584
|
+
derivado deve ser versionado. O manifesto \`source-manifest-v1\` hasheia todo
|
|
585
|
+
JS/TS analisado, mesmo sem entradas; zero arquivo elegível falha porque não
|
|
586
|
+
comprova que o extrator executou sobre uma fonte.
|
|
587
|
+
|
|
588
|
+
Flags:
|
|
589
|
+
--check Compara sem escrever; falha em ausência, drift ou copy dinâmica
|
|
590
|
+
`)
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
await cmdCopy(rest[0], flags)
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
527
597
|
if (command === 'pre-push') {
|
|
528
598
|
if (rest[0] === 'materialization') await cmdMaterializationCheck()
|
|
529
599
|
else await cmdCheck(rest[0] ?? '.')
|
package/bin/lib/check.mjs
CHANGED
|
@@ -21,9 +21,14 @@
|
|
|
21
21
|
* É a régua do reviewer e a métrica de padrão.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { promises as fs } from 'node:fs'
|
|
25
24
|
import path from 'node:path'
|
|
26
25
|
import ts from 'typescript'
|
|
26
|
+
import {
|
|
27
|
+
canonicalProjectDirectory,
|
|
28
|
+
readProjectDirectory,
|
|
29
|
+
readProjectFile,
|
|
30
|
+
safeProjectPath,
|
|
31
|
+
} from '@softize/base/project-path'
|
|
27
32
|
|
|
28
33
|
// Ordem canônica (grupos do ActionBase em src/core/types.ts). Campos kind-específicos
|
|
29
34
|
// (fields/paginate/filters/expand/background/emits/successStatus…) NÃO entram no mapa —
|
|
@@ -480,63 +485,70 @@ const UI_SKIP_DIRS = new Set([...SKIP_DIRS, '__tests__', '__fixtures__', 'fixtur
|
|
|
480
485
|
const UI_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.css', '.html', '.svelte', '.vue'])
|
|
481
486
|
|
|
482
487
|
export async function walkTsFiles(dir, acc = []) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
entries =
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
488
|
+
const root = canonicalProjectDirectory(dir)
|
|
489
|
+
const walk = (local = '.') => {
|
|
490
|
+
const entries = readProjectDirectory(root, local).entries
|
|
491
|
+
for (const e of entries) {
|
|
492
|
+
if (e.name.startsWith('.') && e.name !== '.') continue
|
|
493
|
+
if (SKIP_DIRS.has(e.name)) continue
|
|
494
|
+
const relative = path.join(local, e.name)
|
|
495
|
+
const full = path.join(root, relative)
|
|
496
|
+
if (e.isSymbolicLink()) {
|
|
497
|
+
// Dirent não revela se o link aponta para arquivo ou diretório. Ignorá-lo pela
|
|
498
|
+
// extensão permitiria ocultar uma árvore de sources sob um nome de asset.
|
|
499
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
500
|
+
}
|
|
501
|
+
if (e.isDirectory()) {
|
|
502
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
503
|
+
walk(relative)
|
|
504
|
+
} else if (
|
|
505
|
+
(e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&
|
|
506
|
+
!e.name.endsWith('.d.ts') &&
|
|
507
|
+
!e.name.endsWith('.test.ts')
|
|
508
|
+
) {
|
|
509
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
510
|
+
acc.push(full)
|
|
511
|
+
}
|
|
500
512
|
}
|
|
501
513
|
}
|
|
514
|
+
walk()
|
|
502
515
|
return acc
|
|
503
516
|
}
|
|
504
517
|
|
|
505
518
|
async function walkUiFiles(dir, acc = []) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
entries =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
+
const root = canonicalProjectDirectory(dir)
|
|
520
|
+
const walk = (local = '.') => {
|
|
521
|
+
const entries = readProjectDirectory(root, local).entries
|
|
522
|
+
for (const e of entries) {
|
|
523
|
+
if (e.name.startsWith('.')) continue
|
|
524
|
+
if (UI_SKIP_DIRS.has(e.name)) continue
|
|
525
|
+
const relative = path.join(local, e.name)
|
|
526
|
+
const full = path.join(root, relative)
|
|
527
|
+
if (e.isSymbolicLink()) {
|
|
528
|
+
// Um link com nome de asset ainda pode esconder um diretório com UI analisável.
|
|
529
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
530
|
+
}
|
|
531
|
+
if (e.isDirectory()) {
|
|
532
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
533
|
+
walk(relative)
|
|
534
|
+
} else if (UI_EXTENSIONS.has(path.extname(e.name)) && !/\.(?:test|spec)\.[^.]+$/.test(e.name)) {
|
|
535
|
+
safeProjectPath(root, relative, { mustExist: true })
|
|
536
|
+
acc.push(full)
|
|
537
|
+
}
|
|
519
538
|
}
|
|
520
539
|
}
|
|
540
|
+
walk()
|
|
521
541
|
return acc
|
|
522
542
|
}
|
|
523
543
|
|
|
524
|
-
async function exists(p) {
|
|
525
|
-
try {
|
|
526
|
-
await fs.access(p)
|
|
527
|
-
return true
|
|
528
|
-
} catch {
|
|
529
|
-
return false
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
544
|
/** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
|
|
534
545
|
* `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
|
|
535
546
|
export async function scanDir(rootDir) {
|
|
547
|
+
rootDir = canonicalProjectDirectory(rootDir)
|
|
536
548
|
const files = await walkTsFiles(rootDir)
|
|
537
549
|
const sources = []
|
|
538
550
|
for (const file of files) {
|
|
539
|
-
const text =
|
|
551
|
+
const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
|
|
540
552
|
if (!ACTION_MARKERS.some((m) => text.includes(m))) continue
|
|
541
553
|
sources.push({ file: path.relative(rootDir, file), text })
|
|
542
554
|
}
|
|
@@ -545,10 +557,10 @@ export async function scanDir(rootDir) {
|
|
|
545
557
|
// UI pode morar em src/, app/, pages/ ou na raiz. O walk cobre o projeto inteiro e
|
|
546
558
|
// exclui dependências, builds, testes e fixtures para não interpretar exemplos como uso.
|
|
547
559
|
for (const file of await walkUiFiles(rootDir)) {
|
|
548
|
-
const text =
|
|
560
|
+
const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
|
|
549
561
|
uiFindings.push(...checkUiMigrations(path.relative(rootDir, file), text))
|
|
550
562
|
}
|
|
551
|
-
const hasMarker =
|
|
563
|
+
const hasMarker = safeProjectPath(rootDir, 'opus.json').exists
|
|
552
564
|
return {
|
|
553
565
|
files: files.length,
|
|
554
566
|
actions: checked.actions,
|