@softize/opus 12.6.3 → 12.7.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 (34) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +20 -2
  3. package/bin/cli.mjs +10 -1
  4. package/bin/lib/gen-manifest.mjs +1 -0
  5. package/bin/lib/gen-runner.mjs +29 -1
  6. package/bin/lib/seed-runner.mjs +152 -0
  7. package/bin/lib/seed.mjs +229 -0
  8. package/docs/adr/0002-structured-seeds-are-declared-and-bound.md +130 -0
  9. package/docs/protocol.md +10 -1
  10. package/docs/seeds.md +132 -0
  11. package/package.json +5 -1
  12. package/registry/instructions/opus.md +4 -0
  13. package/registry/skills/apply-opus-seed/SKILL.md +45 -0
  14. package/registry/skills/apply-opus-seed/agents/openai.yaml +4 -0
  15. package/registry/skills/apply-opus-seed/references/evaluations.md +8 -0
  16. package/registry/skills/create-opus-seed/SKILL.md +55 -0
  17. package/registry/skills/create-opus-seed/agents/openai.yaml +4 -0
  18. package/registry/skills/create-opus-seed/references/contract.md +16 -0
  19. package/registry/skills/create-opus-seed/references/evaluations.md +8 -0
  20. package/registry/skills/create-opus-seed/scripts/scaffold.mjs +78 -0
  21. package/registry/skills/implement-opus-change/SKILL.md +3 -1
  22. package/src/core/index.ts +2 -0
  23. package/src/core/types.ts +16 -1
  24. package/src/seed/index.ts +391 -0
  25. package/src/ui/components/patterns/form.tsx +131 -19
  26. package/src/ui/components/primitives/detail.tsx +113 -0
  27. package/src/ui/docs/content/action-form.md +10 -0
  28. package/src/ui/docs/content/cli.md +17 -0
  29. package/src/ui/docs/content/detail.md +38 -0
  30. package/src/ui/docs/registry.tsx +2 -0
  31. package/src/ui/drivers/react.tsx +3 -2
  32. package/src/ui/lib/object-schema.ts +36 -0
  33. package/src/ui/meta.ts +7 -1
  34. package/src/ui/react.tsx +9 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ 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
+ ## 12.7.0 — 2026-08-29
11
+
12
+ Seeds persistentes ganham uma porta própria em `@softize/opus/seed`: declaração compartilhável,
13
+ binding server-only, perfis, dependências, métricas esperadas e escopos permitidos. A CLI passa a
14
+ listar, validar, planejar, aplicar e verificar esses datasets por `opus seed`, bloqueia qualquer
15
+ operação com banco sem ambiente seguro e atestação do alvo real, e reprova comandos locais
16
+ paralelos ou compostos no gate.
17
+
18
+ O registry inclui as skills `create-opus-seed` e `apply-opus-seed`. O manifest projeta somente a
19
+ declaração; conexão e funções do binding permanecem server-only. A mudança é aditiva. Projetos que
20
+ adotarem a superfície devem migrar scripts `seed*`/`db:seed*` ou fazê-los delegar para a CLI.
21
+
22
+ `DetailField` e `DetailGroup` oferecem a composição canônica para dados somente leitura, com
23
+ labels, valores React ricos, grade responsiva, moldura externa e divisórias internas opcionais.
24
+ `ActionForm` também passa a renderizar `widget: 'toggle-group'` de forma declarativa e aceita
25
+ conteúdo rico nas opções pelo mesmo contrato usado pelos demais campos. O novo `FieldWidget` tipa
26
+ os overrides reconhecidos pelo renderer. As mudanças são aditivas.
27
+
10
28
  ## 12.6.3 — 2026-08-26
11
29
 
12
30
  A skill `build-opus-ui` passa a orientar operacionalmente os patterns entregues em 12.6.0 e
@@ -46,7 +64,6 @@ Os patterns existentes passam a reutilizar os primitives canônicos: `ActionForm
46
64
  `FieldGroup`, descrições e erros associados ao controle; `ActionList` usa a moldura de `Table` e
47
65
  os estados seguros de `DataState`, preservando a tentativa de recuperação; e a confirmação de uma
48
66
  `ActionTrigger` destrutiva mantém o tratamento destrutivo mesmo quando o gatilho visual é discreto.
49
-
50
67
  ## 12.5.5 — 2026-08-25
51
68
 
52
69
  Só documentação: leva ao pacote a correção de nota que a 12.5.4 recebeu no repositório depois de
package/README.md CHANGED
@@ -35,6 +35,7 @@ Um pacote (`@softize/opus`), uma versão. Cada categoria abaixo é um **subpath
35
35
  | `@softize/opus/queue` | `/bullmq` | Background job execution |
36
36
  | `@softize/opus/events` | `/mitt` | EventBus (in-process) |
37
37
  | `@softize/opus/scheduler` | `/node-cron` | Schedule adapter (ação iniciada por tempo) |
38
+ | `@softize/opus/seed` | — | Declaração e binding de datasets verificáveis operados pela CLI |
38
39
 
39
40
  Drivers via subpath ESM (estilo Drizzle): `@softize/opus/server/fastify`, `@softize/opus/data/kysely`.
40
41
 
@@ -82,12 +83,29 @@ src/
82
83
  core/ schema/ server/ client/ ui/
83
84
  data/ auth/ audit/ log/ queue/ events/ scheduler/ dsl/
84
85
  registry/ # scaffolds + skills Opus — geração e conhecimento do SDK, não runtime
85
- bin/ # CLI (opus setup/gen/copy/check/introspect/mcp) + libs
86
+ bin/ # CLI (opus setup/gen/copy/check/seed/introspect/mcp) + libs
86
87
  docs/
87
88
  protocol.md # contrato completo (16 seções)
88
- data-layer.md · releasing.md · code-style.md
89
+ data-layer.md · seeds.md · releasing.md · code-style.md
89
90
  ```
90
91
 
92
+ ## Seeds de projeto
93
+
94
+ Datasets persistentes de desenvolvimento, teste e demonstração usam `defineSeed` + `bindSeed` e
95
+ ficam registrados em `opus.config.ts`. A CLI descobre e valida sem abrir conexão, exige escopo
96
+ explícito para operar e bloqueia produção:
97
+
98
+ ```bash
99
+ pnpm exec opus seed list
100
+ pnpm exec opus seed check
101
+ pnpm exec opus seed plan customers.scenarios --profile smoke --scope local
102
+ pnpm exec opus seed apply customers.scenarios --profile smoke --scope local
103
+ pnpm exec opus seed verify customers.scenarios --profile smoke --scope local
104
+ ```
105
+
106
+ O contrato não oferece reset/truncate, e `apply` precisa convergir quando repetido. Veja
107
+ [docs/seeds.md](docs/seeds.md).
108
+
91
109
  ## Status
92
110
 
93
111
  - **v0**: 12 superfícies, 430 tests, 100% coverage.
package/bin/cli.mjs CHANGED
@@ -31,6 +31,7 @@ import { createMonorepo, createProject } from './lib/create.mjs'
31
31
  import { initProject, repoRootOf, setupUiFoundation } from './lib/init.mjs'
32
32
  import { introspect } from './lib/introspect.mjs'
33
33
  import { materializeOpus } from './lib/materialize.mjs'
34
+ import { cmdSeed } from './lib/seed.mjs'
34
35
 
35
36
  const __filename = fileURLToPath(import.meta.url)
36
37
  const __dirname = path.dirname(__filename)
@@ -393,7 +394,7 @@ function parseArgs(argv) {
393
394
  const positional = []
394
395
  const flags = { force: false, help: false }
395
396
  // Flags com valor — pega o próximo arg.
396
- const VALUE_FLAGS = new Set(['--config', '--output'])
397
+ const VALUE_FLAGS = new Set(['--config', '--output', '--profile', '--scope'])
397
398
  for (let i = 0; i < args.length; i++) {
398
399
  const arg = args[i]
399
400
  if (arg === '--force' || arg === '-f') {
@@ -455,6 +456,7 @@ Comandos:
455
456
  check [dir] Valida as convenções das actions (régua de padrão; exit ≠ 0 se violar)
456
457
  pre-push Gate Git: freshness dos artefatos + convenções das actions
457
458
  db <verbo> Comandos de banco. v1: db check (drift-check entidade ↔ banco)
459
+ seed <verbo> Lista, valida, planeja, aplica e verifica seeds estruturados
458
460
  introspect [dir] Modelo da estrutura (actions/reactions/schedules + wiring); --json
459
461
  mcp Server MCP (introspect/check/scaffold de action) — agentes via mcp_config
460
462
  help Mostra esta mensagem
@@ -464,6 +466,8 @@ Flags:
464
466
  --check Não escreve; falha se o inventário de copy estiver ausente/desatualizado
465
467
  --config <path> (gen) Caminho do opus.config.ts
466
468
  --output <path> (gen) Pasta de saída
469
+ --profile <name> (seed) Perfil do dataset
470
+ --scope <name> (seed) Escopo explícito dos dados
467
471
 
468
472
  Exemplos:
469
473
  npx @softize/opus add action-form
@@ -606,6 +610,11 @@ Flags:
606
610
  return
607
611
  }
608
612
 
613
+ if (command === 'seed') {
614
+ await cmdSeed(rest, flags)
615
+ return
616
+ }
617
+
609
618
  if (command === 'introspect') {
610
619
  await cmdIntrospect(rest[0], flags)
611
620
  return
@@ -14,6 +14,7 @@ export function buildManifest(payload) {
14
14
  return {
15
15
  version: '1',
16
16
  opusVersion: payload.opusVersion,
17
+ seeds: payload.seeds ?? [],
17
18
  domains: payload.domains.map(shapeDomain),
18
19
  }
19
20
  }
@@ -24,7 +24,8 @@
24
24
  * opusVersion: string,
25
25
  * sourceConfigDir: string, // dirname absoluto do opus.config.ts
26
26
  * output: string, // pasta de saída declarada no config
27
- * domains: SerializedDomain[]
27
+ * domains: SerializedDomain[],
28
+ * seeds: SerializedSeed[]
28
29
  * }
29
30
  *
30
31
  * SerializedDomain:
@@ -87,6 +88,12 @@ async function main() {
87
88
  ).href
88
89
  const { flattenDomain } = await import(opusCoreUrl)
89
90
 
91
+ const opusSeedUrl = pathToFileURL(
92
+ path.join(PACKAGE_ROOT, 'src', 'seed', 'index.ts'),
93
+ ).href
94
+ const { checkSeedRegistry, isSeedDefinition, publicSeedDefinition } =
95
+ await import(opusSeedUrl)
96
+
90
97
  // Acessar logical type meta dos dicts.
91
98
  const opusSchemaUrl = pathToFileURL(
92
99
  path.join(PACKAGE_ROOT, 'src', 'schema', 'index.ts'),
@@ -127,17 +134,38 @@ async function main() {
127
134
  }
128
135
 
129
136
  const domains = cfg.domains.map((d) => serializeDomain(d, ctx))
137
+ if (cfg.seeds !== undefined && !Array.isArray(cfg.seeds)) {
138
+ throw new Error('config.seeds inválido — use um array de declarações registradas')
139
+ }
140
+ const registeredSeeds = cfg.seeds ?? []
141
+ const seedCheck = checkSeedRegistry(registeredSeeds)
142
+ if (!seedCheck.ok) {
143
+ throw new Error(
144
+ `config.seeds inválido — ${seedCheck.diagnostics.map((diagnostic) => diagnostic.message).join('; ')}`,
145
+ )
146
+ }
147
+ const seeds = registeredSeeds.map((seed) =>
148
+ serializeSeed(seed, isSeedDefinition, publicSeedDefinition),
149
+ )
130
150
 
131
151
  const payload = {
132
152
  opusVersion: pkg.version,
133
153
  sourceConfigDir: ctx.sourceConfigDir,
134
154
  output: outputDir,
135
155
  domains,
156
+ seeds,
136
157
  }
137
158
 
138
159
  emit({ ok: true, payload })
139
160
  }
140
161
 
162
+ function serializeSeed(seed, isSeedDefinition, publicSeedDefinition) {
163
+ if (!isSeedDefinition(seed)) {
164
+ throw new Error('config.seeds contém uma declaração inválida')
165
+ }
166
+ return publicSeedDefinition(seed)
167
+ }
168
+
141
169
  // =============================================================================
142
170
  // Serialização
143
171
  // =============================================================================
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /** Carrega seeds TypeScript do consumidor num subprocesso isolado via tsx. */
3
+
4
+ import path from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ import {
8
+ assertSeedExecutionAllowed,
9
+ assertSeedExpectedMetrics,
10
+ checkSeedRegistry,
11
+ publicSeedDefinition,
12
+ resolveSeedProfile,
13
+ validateSeedPlan,
14
+ validateSeedReport,
15
+ } from '../../src/seed/index.ts'
16
+
17
+ function emit(value) {
18
+ process.stdout.write(`\n${JSON.stringify(value)}\n`)
19
+ }
20
+
21
+ function publicDefinition(seed) {
22
+ return publicSeedDefinition(seed)
23
+ }
24
+
25
+ async function main() {
26
+ const [configArg, command, name, requestedProfile, requestedScope] = process.argv.slice(2)
27
+ if (typeof configArg !== 'string') throw new Error('config path ausente')
28
+
29
+ const module = await import(pathToFileURL(path.resolve(configArg)).href)
30
+ const config = module.default ?? module.config ?? module
31
+ const seeds = config.seeds
32
+ if (!Array.isArray(seeds) || seeds.length === 0) {
33
+ throw new Error('nenhum seed registrado em config.seeds')
34
+ }
35
+
36
+ const check = checkSeedRegistry(seeds)
37
+ const targetDiagnostics =
38
+ typeof config.assertSeedTarget === 'function'
39
+ ? []
40
+ : [
41
+ {
42
+ code: 'seed.missing_target_assertion',
43
+ seed: 'opus.config.ts',
44
+ message:
45
+ 'opus.config.ts precisa de assertSeedTarget para atestar a conexão antes dos bindings',
46
+ },
47
+ ]
48
+ if (command === 'check') {
49
+ emit({
50
+ ok: check.ok && targetDiagnostics.length === 0,
51
+ diagnostics: [...check.diagnostics, ...targetDiagnostics],
52
+ seeds: seeds.length,
53
+ })
54
+ return
55
+ }
56
+ if (!check.ok) {
57
+ emit({ ok: false, diagnostics: check.diagnostics })
58
+ return
59
+ }
60
+ if (command === 'list') {
61
+ emit({ ok: true, seeds: check.ordered.map(publicDefinition) })
62
+ return
63
+ }
64
+ if (targetDiagnostics.length > 0) {
65
+ emit({ ok: false, diagnostics: targetDiagnostics })
66
+ return
67
+ }
68
+ if (command !== 'plan' && command !== 'apply' && command !== 'verify') {
69
+ throw new Error(`operação de seed desconhecida: ${String(command)}`)
70
+ }
71
+
72
+ const seed = check.ordered.find((candidate) => candidate.name === name)
73
+ if (seed === undefined) {
74
+ throw new Error(`seed ${String(name)} não encontrado`)
75
+ }
76
+ const profile = resolveSeedProfile(seed, requestedProfile || undefined)
77
+ const scope = requestedScope || undefined
78
+ const executionSeeds = dependencyClosure(check.ordered, seed)
79
+ const selections = executionSeeds.map((candidate) => ({
80
+ seed: candidate,
81
+ profile: candidate.name === seed.name ? profile : resolveSeedProfile(candidate),
82
+ }))
83
+ for (const selection of selections) assertSeedExecutionAllowed(selection.seed, scope)
84
+
85
+ const database = typeof config.database === 'function' ? await config.database() : null
86
+ try {
87
+ const targetAttested = await config.assertSeedTarget({ database, scope })
88
+ if (targetAttested !== true) {
89
+ throw new Error('assertSeedTarget precisa retornar true após atestar o destino real')
90
+ }
91
+ if (command === 'plan') {
92
+ const steps = []
93
+ for (const selection of selections) {
94
+ const plan = validateSeedPlan(await selection.seed.binding.plan({
95
+ database,
96
+ profile: selection.profile,
97
+ scope,
98
+ }))
99
+ steps.push({ seed: publicDefinition(selection.seed), profile: selection.profile, plan })
100
+ }
101
+ emit({
102
+ ok: true,
103
+ command,
104
+ seed: publicDefinition(seed),
105
+ profile,
106
+ scope,
107
+ plan: steps.at(-1).plan,
108
+ steps,
109
+ })
110
+ return
111
+ }
112
+ const steps = []
113
+ for (const selection of selections) {
114
+ const report = validateSeedReport(await selection.seed.binding[command]({
115
+ database,
116
+ profile: selection.profile,
117
+ scope,
118
+ }))
119
+ if (command === 'verify') {
120
+ assertSeedExpectedMetrics(selection.seed, selection.profile, report)
121
+ }
122
+ steps.push({ seed: publicDefinition(selection.seed), profile: selection.profile, report })
123
+ }
124
+ emit({
125
+ ok: true,
126
+ command,
127
+ seed: publicDefinition(seed),
128
+ profile,
129
+ scope,
130
+ report: steps.at(-1).report,
131
+ steps,
132
+ })
133
+ } finally {
134
+ if (database !== null && typeof database.destroy === 'function') await database.destroy()
135
+ }
136
+ }
137
+
138
+ function dependencyClosure(ordered, target) {
139
+ const required = new Set()
140
+ const byName = new Map(ordered.map((seed) => [seed.name, seed]))
141
+ const collect = (seed) => {
142
+ if (required.has(seed.name)) return
143
+ for (const dependency of seed.dependsOn ?? []) collect(byName.get(dependency))
144
+ required.add(seed.name)
145
+ }
146
+ collect(target)
147
+ return ordered.filter((seed) => required.has(seed.name))
148
+ }
149
+
150
+ main().catch((error) => {
151
+ emit({ ok: false, error: error instanceof Error ? error.message : String(error) })
152
+ })
@@ -0,0 +1,229 @@
1
+ /** Grupo `opus seed`: descoberta, gate e execução segura dos seeds do projeto. */
2
+
3
+ import { execFile } from 'node:child_process'
4
+ import { promises as fs } from 'node:fs'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { promisify } from 'node:util'
8
+
9
+ import { canonicalProjectDirectory, readProjectFile, safeProjectPath } from '@softize/base/project-path'
10
+
11
+ const execFileAsync = promisify(execFile)
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = path.dirname(__filename)
14
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
15
+
16
+ const COLORS = {
17
+ info: '\x1b[36m',
18
+ success: '\x1b[32m',
19
+ error: '\x1b[31m',
20
+ warn: '\x1b[33m',
21
+ dim: '\x1b[2m',
22
+ }
23
+ const RESET = '\x1b[0m'
24
+
25
+ function log(level, message) {
26
+ console.log(`${COLORS[level] ?? ''}${message}${RESET}`)
27
+ }
28
+
29
+ export function findAdHocSeedScripts(scripts = {}) {
30
+ return Object.entries(scripts).flatMap(([name, command]) => {
31
+ const isSeedName = /^seed(?::|$)/.test(name) || /^db:seed(?::|$)/.test(name)
32
+ const hasShellComposition =
33
+ typeof command === 'string' && /(?:&&|\|\||[;&|<>`\n\r]|\$\()/.test(command)
34
+ const delegatesToOpus =
35
+ typeof command === 'string' &&
36
+ !hasShellComposition &&
37
+ /^(?:npx |pnpm exec )?opus seed (?:list|check|plan|apply|verify)(?:\s|$)/.test(command.trim())
38
+ return isSeedName && !delegatesToOpus ? [{ name, command }] : []
39
+ })
40
+ }
41
+
42
+ export async function cmdSeed(rest, flags) {
43
+ const [command, name] = rest
44
+ if (flags.help || command === undefined || command === 'help') {
45
+ helpSeed()
46
+ return
47
+ }
48
+ if (!['list', 'check', 'plan', 'apply', 'verify'].includes(command)) {
49
+ fail(flags, `Subcomando seed desconhecido: ${command}`)
50
+ if (!flags.json) helpSeed()
51
+ return
52
+ }
53
+ if (['plan', 'apply', 'verify'].includes(command) && typeof name !== 'string') {
54
+ fail(flags, `Uso: opus seed ${command} <nome> --scope <escopo>`)
55
+ return
56
+ }
57
+
58
+ let resolved
59
+ try {
60
+ resolved = resolveConfig(flags)
61
+ } catch (error) {
62
+ fail(flags, error instanceof Error ? error.message : String(error))
63
+ return
64
+ }
65
+ const { cwd, configPath } = resolved
66
+ const result = await runRunner(configPath, [
67
+ command,
68
+ name ?? '',
69
+ flags.profile ?? '',
70
+ flags.scope ?? process.env.OPUS_SEED_SCOPE ?? '',
71
+ ])
72
+
73
+ if (command === 'check') {
74
+ const scriptFindings = readSeedScriptFindings(cwd)
75
+ const runnerFindings = result.error === undefined ? [] : [{
76
+ code: 'seed.invalid_registry',
77
+ seed: 'opus.config.ts',
78
+ message: result.error,
79
+ }]
80
+ const diagnostics = [...(result.diagnostics ?? []), ...runnerFindings, ...scriptFindings]
81
+ const checked = { ...result, ok: result.ok === true && diagnostics.length === 0, diagnostics }
82
+ if (flags.json) {
83
+ console.log(JSON.stringify(checked, null, 2))
84
+ } else if (checked.ok) {
85
+ log('success', `✓ seed check: ${checked.seeds} seed(s) estruturado(s); registro consistente.`)
86
+ } else {
87
+ printDiagnostics(diagnostics)
88
+ log('error', `✗ seed check: ${diagnostics.length} violação(ões).`)
89
+ }
90
+ if (!checked.ok) process.exitCode = 1
91
+ return
92
+ }
93
+
94
+ if (result.ok !== true) {
95
+ if (flags.json) console.log(JSON.stringify(result, null, 2))
96
+ else {
97
+ printDiagnostics(result.diagnostics ?? [])
98
+ log('error', `✗ opus seed ${command}: ${result.error ?? 'registro inconsistente'}`)
99
+ }
100
+ process.exitCode = 1
101
+ return
102
+ }
103
+
104
+ if (flags.json) {
105
+ console.log(JSON.stringify(result, null, 2))
106
+ return
107
+ }
108
+ if (command === 'list') printList(result.seeds)
109
+ else if (command === 'plan') printPlan(result)
110
+ else printReport(result)
111
+ }
112
+
113
+ function fail(flags, message) {
114
+ if (flags.json) console.log(JSON.stringify({ ok: false, error: message }, null, 2))
115
+ else log('error', `✗ opus seed: ${message}`)
116
+ process.exitCode = 1
117
+ }
118
+
119
+ function resolveConfig(flags) {
120
+ const cwd = canonicalProjectDirectory(process.cwd())
121
+ const configRel = flags.config ?? 'opus.config.ts'
122
+ const config = safeProjectPath(cwd, configRel)
123
+ if (!config.exists) throw new Error(`opus.config.ts não encontrado em ${config.path}`)
124
+ return { cwd, configPath: config.path }
125
+ }
126
+
127
+ function readSeedScriptFindings(cwd) {
128
+ const packageFile = readProjectFile(cwd, 'package.json', { allowMissing: true })
129
+ if (!packageFile.exists) return []
130
+ let parsed
131
+ try {
132
+ parsed = JSON.parse(packageFile.content)
133
+ } catch {
134
+ return [{ code: 'seed.invalid_package', seed: 'package.json', message: 'package.json não é JSON válido' }]
135
+ }
136
+ return findAdHocSeedScripts(parsed.scripts).map(({ name, command }) => ({
137
+ code: 'seed.ad_hoc_script',
138
+ seed: name,
139
+ message: `Script ${name} não delega para opus seed: ${String(command)}`,
140
+ }))
141
+ }
142
+
143
+ async function runRunner(configPath, extraArgs) {
144
+ const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
145
+ const command = (await fileExists(tsxBin)) ? tsxBin : 'tsx'
146
+ const runnerPath = path.join(__dirname, 'seed-runner.mjs')
147
+ try {
148
+ const { stdout } = await execFileAsync(command, [runnerPath, configPath, ...extraArgs], {
149
+ encoding: 'utf8',
150
+ maxBuffer: 16 * 1024 * 1024,
151
+ env: { ...process.env },
152
+ })
153
+ const line = lastJsonLine(stdout)
154
+ return line === null ? { ok: false, error: 'runner não emitiu JSON' } : JSON.parse(line)
155
+ } catch (error) {
156
+ const detail = typeof error.stderr === 'string' && error.stderr.trim() ? error.stderr.trim() : error.message
157
+ return { ok: false, error: detail }
158
+ }
159
+ }
160
+
161
+ function printList(seeds) {
162
+ log('info', `\nSeeds estruturados (${seeds.length}):`)
163
+ for (const seed of seeds) {
164
+ console.log(` ${seed.name}@${seed.version} — ${seed.description}`)
165
+ console.log(` perfis: ${Object.keys(seed.profiles).join(', ')}; default: ${seed.defaultProfile}`)
166
+ console.log(` escopos: ${seed.safety.scopes.join(', ')}`)
167
+ }
168
+ console.log('')
169
+ }
170
+
171
+ function printPlan(result) {
172
+ log('info', `→ ${result.seed.name}@${result.seed.version} [${result.profile}] em ${result.scope}`)
173
+ for (const step of result.steps) {
174
+ console.log(` ${step.seed.name} [${step.profile}] — ${step.plan.summary}`)
175
+ for (const operation of step.plan.operations) console.log(` - ${operation}`)
176
+ }
177
+ }
178
+
179
+ function printReport(result) {
180
+ log('success', `✓ seed ${result.command}: ${result.seed.name}@${result.seed.version} [${result.profile}]`)
181
+ for (const step of result.steps) {
182
+ console.log(` ${step.seed.name} [${step.profile}] — ${step.report.summary}`)
183
+ for (const [metric, value] of Object.entries(step.report.metrics)) console.log(` ${metric}: ${value}`)
184
+ }
185
+ }
186
+
187
+ function printDiagnostics(diagnostics) {
188
+ for (const diagnostic of diagnostics) log('warn', `[${diagnostic.code}] ${diagnostic.message}`)
189
+ }
190
+
191
+ function lastJsonLine(stdout) {
192
+ const lines = stdout.split('\n').filter((line) => line.trim())
193
+ for (let index = lines.length - 1; index >= 0; index--) {
194
+ const line = lines[index].trim()
195
+ if (line.startsWith('{') && line.endsWith('}')) return line
196
+ }
197
+ return null
198
+ }
199
+
200
+ async function fileExists(file) {
201
+ try {
202
+ await fs.access(file)
203
+ return true
204
+ } catch {
205
+ return false
206
+ }
207
+ }
208
+
209
+ export function helpSeed() {
210
+ console.log(`
211
+ @softize/opus seed — datasets estruturados do projeto
212
+
213
+ seed list Lista seeds, perfis, versões e escopos sem abrir o banco.
214
+ seed check Valida bindings, dependências, ciclos e scripts paralelos.
215
+ seed plan <nome> Mostra as operações previstas sem escrever.
216
+ seed apply <nome> Aplica o perfil de forma convergente.
217
+ seed verify <nome> Confere métricas e invariantes do perfil.
218
+
219
+ Flags:
220
+ --config <path> Caminho interno ao projeto para opus.config.ts.
221
+ --profile <nome> Perfil; usa defaultProfile quando omitido.
222
+ --scope <escopo> Escopo explícito dos dados. Alternativa: OPUS_SEED_SCOPE.
223
+ --json Retorna o resultado estruturado.
224
+
225
+ \`plan\`, \`apply\` e \`verify\` só abrem conexão com NODE_ENV=development ou NODE_ENV=test.
226
+ O contrato não possui reset nem truncate; aplicação repetida precisa convergir para o mesmo estado
227
+ observável.
228
+ `)
229
+ }