@softize/opus 24.1.0 → 25.1.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 +121 -0
- package/README.md +1 -1
- package/bin/cli.mjs +9 -0
- package/bin/lib/check.mjs +390 -1
- package/bin/lib/copy.mjs +39 -2
- package/bin/lib/gen-runner.mjs +25 -12
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0051-soft-delete-declares-retention-and-purge.md +224 -0
- package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
- package/docs/data-layer.md +31 -3
- package/docs/protocol.md +98 -2
- package/docs/releasing.md +8 -2
- package/docs/stacking-scale.md +53 -0
- package/package.json +6 -1
- package/src/ai/drivers/anthropic.ts +3 -0
- package/src/core/domain.ts +189 -7
- package/src/core/index.ts +19 -2
- package/src/core/retention.ts +493 -0
- package/src/core/runtime.ts +93 -29
- package/src/core/system-actor.ts +33 -0
- package/src/core/types.ts +12 -0
- package/src/data/drivers/kysely.ts +47 -5
- package/src/data/index.ts +24 -6
- package/src/mcp/index.ts +3 -2
- package/src/schema/entity.ts +30 -9
- package/src/schema/index.ts +4 -0
- package/src/schema/openapi.ts +4 -1
- package/src/ui/components/primitives/dialog.tsx +285 -26
- package/src/ui/components/primitives/drawer.tsx +104 -13
- package/src/ui/components/primitives/floating-layer.tsx +79 -0
- package/src/ui/components/primitives/menu.tsx +17 -2
- package/src/ui/components/primitives/popover.tsx +6 -1
- package/src/ui/components/primitives/tooltip.tsx +6 -1
- package/src/ui/docs/content/cli.md +5 -3
- package/src/ui/docs/content/data.md +42 -0
- package/src/ui/docs/content/dialog.md +3 -0
- package/src/ui/docs/content/drawer.md +3 -0
- package/src/ui/docs/content/floating-layer.md +178 -0
- package/src/ui/docs/content/scheduler.md +4 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/lib/dismiss-on-iframe.ts +4 -1
- package/src/ui/lib/modal-layers.ts +547 -0
- package/src/ui/meta.ts +6 -0
- package/src/ui/react.tsx +4 -0
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -90,7 +90,7 @@ async function main() {
|
|
|
90
90
|
const opusCoreUrl = pathToFileURL(
|
|
91
91
|
path.join(PACKAGE_ROOT, "src", "core", "index.ts"),
|
|
92
92
|
).href;
|
|
93
|
-
const { flattenDomain } = await import(opusCoreUrl);
|
|
93
|
+
const { flattenDomain, domainPurges, declaredJsonSchema } = await import(opusCoreUrl);
|
|
94
94
|
|
|
95
95
|
const opusSeedUrl = pathToFileURL(
|
|
96
96
|
path.join(PACKAGE_ROOT, "src", "seed", "index.ts"),
|
|
@@ -137,7 +137,10 @@ async function main() {
|
|
|
137
137
|
|
|
138
138
|
const ctx = {
|
|
139
139
|
flattenDomain,
|
|
140
|
+
domainPurges,
|
|
141
|
+
// Standard Schema que declara o próprio JSON Schema (ADR 0051) dispensa o zod.
|
|
140
142
|
toJsonSchema: (schema) =>
|
|
143
|
+
declaredJsonSchema(schema, { target: "openapi-3.0" }) ??
|
|
141
144
|
zodToJsonSchema(schema, { target: "openApi3", $refStrategy: "none" }),
|
|
142
145
|
sourceConfigDir: path.dirname(configAbs),
|
|
143
146
|
};
|
|
@@ -150,7 +153,7 @@ async function main() {
|
|
|
150
153
|
}
|
|
151
154
|
const actionRegistry = Object.fromEntries(
|
|
152
155
|
cfg.domains
|
|
153
|
-
.flatMap((domain) => collectDomainActions(domain))
|
|
156
|
+
.flatMap((domain) => collectDomainActions(domain, ctx))
|
|
154
157
|
.map((action) => [action.name, action]),
|
|
155
158
|
);
|
|
156
159
|
const presentations = validatePresentations(
|
|
@@ -185,12 +188,15 @@ async function main() {
|
|
|
185
188
|
emit({ ok: true, payload });
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
function collectDomainActions(domain) {
|
|
191
|
+
function collectDomainActions(domain, ctx, parentPath = []) {
|
|
189
192
|
const actions = Array.from(iterateActions(domain.actions));
|
|
193
|
+
for (const purge of ctx.domainPurges(domain, parentPath)) actions.push(purge.action);
|
|
190
194
|
for (const subdomain of Array.isArray(domain.subdomains)
|
|
191
195
|
? domain.subdomains
|
|
192
196
|
: []) {
|
|
193
|
-
actions.push(
|
|
197
|
+
actions.push(
|
|
198
|
+
...collectDomainActions(subdomain, ctx, [...parentPath, domain.name]),
|
|
199
|
+
);
|
|
194
200
|
}
|
|
195
201
|
return actions;
|
|
196
202
|
}
|
|
@@ -206,8 +212,11 @@ function serializeSeed(seed, isSeedDefinition, publicSeedDefinition) {
|
|
|
206
212
|
// Serialização
|
|
207
213
|
// =============================================================================
|
|
208
214
|
|
|
209
|
-
function serializeDomain(domain, ctx) {
|
|
215
|
+
function serializeDomain(domain, ctx, parentPath = []) {
|
|
210
216
|
const entityList = serializeEntities(domain.entities);
|
|
217
|
+
// Expurgo derivado das entidades com retenção (ADR 0051): a action e a rotina entram no
|
|
218
|
+
// manifest no domínio que lista a entidade, como as declaradas.
|
|
219
|
+
const purges = ctx.domainPurges(domain, parentPath);
|
|
211
220
|
return {
|
|
212
221
|
name: domain.name,
|
|
213
222
|
description: nullable(domain.description),
|
|
@@ -216,17 +225,21 @@ function serializeDomain(domain, ctx) {
|
|
|
216
225
|
entities: entityList,
|
|
217
226
|
dataProducts: serializeDataProducts(domain.dataProducts),
|
|
218
227
|
dicts: serializeDicts(domain.dicts),
|
|
219
|
-
actions:
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
actions: [
|
|
229
|
+
...iterateActions(domain.actions),
|
|
230
|
+
...purges.map((purge) => purge.action),
|
|
231
|
+
].map((a) => serializeAction(a, ctx)),
|
|
222
232
|
reactions: Array.isArray(domain.reactions)
|
|
223
233
|
? domain.reactions.map(serializeReaction)
|
|
224
234
|
: [],
|
|
225
|
-
schedules:
|
|
226
|
-
? domain.schedules
|
|
227
|
-
|
|
235
|
+
schedules: [
|
|
236
|
+
...(Array.isArray(domain.schedules) ? domain.schedules : []),
|
|
237
|
+
...purges.map((purge) => purge.schedule),
|
|
238
|
+
].map(serializeSchedule),
|
|
228
239
|
subdomains: Array.isArray(domain.subdomains)
|
|
229
|
-
? domain.subdomains.map((s) =>
|
|
240
|
+
? domain.subdomains.map((s) =>
|
|
241
|
+
serializeDomain(s, ctx, [...parentPath, domain.name]),
|
|
242
|
+
)
|
|
230
243
|
: [],
|
|
231
244
|
};
|
|
232
245
|
}
|
package/bin/lib/mcp.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export function buildServer() {
|
|
|
42
42
|
server.registerTool(
|
|
43
43
|
'opus_check',
|
|
44
44
|
{
|
|
45
|
-
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind. Retorna findings.',
|
|
45
|
+
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind, soft-delete-retention. Retorna findings.',
|
|
46
46
|
inputSchema: { dir: z.string().optional() },
|
|
47
47
|
},
|
|
48
48
|
async ({ dir }) => asText(await scanDir(dirOf(dir))),
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# ADR 0051 — Exclusão lógica declara retenção e expurgo
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-02.
|
|
5
|
+
- **Complementa:** ADR 0028, que roda actions agendadas como o sistema, e ADR 0002, cujo par
|
|
6
|
+
declaração e binding serve de molde.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
`defineEntity({ softDelete: true })` injeta a coluna `deletedAt`. Pelo `kyselyRepo`, excluir passa a
|
|
11
|
+
marcar a data, e `findById`, `all` e `list` deixam os registros excluídos de fora. O ciclo fica pela
|
|
12
|
+
metade: o Opus sabe excluir de forma lógica, mas nada diz quando o registro sai de vez.
|
|
13
|
+
|
|
14
|
+
Sem esse fechamento, cada consumidor resolve sozinho. A plataforma Grand Brasil é o caso concreto:
|
|
15
|
+
|
|
16
|
+
- os fluxos de automação fazem exclusão lógica e expurgo à mão, com coluna própria e uma action
|
|
17
|
+
`automation.purge-deleted` escrita no domínio. Ela é restrita ao sistema e à administração, é
|
|
18
|
+
interna e roda numa rotina diária;
|
|
19
|
+
- os relatórios ainda apagam de vez. Por isso, excluir um relatório cuja conversa de IA registrou
|
|
20
|
+
uma autorização de extração de dado pessoal esbarra numa escolha ruim: apagar a autorização
|
|
21
|
+
junto, contra a regra que manda guardá-la, ou impedir a exclusão.
|
|
22
|
+
|
|
23
|
+
A forma escolhida para os relatórios é a mesma dos fluxos: excluir de forma lógica, manter o
|
|
24
|
+
histórico durante um prazo e expurgar depois, numa rotina. Com dois casos, o padrão já se repete.
|
|
25
|
+
No terceiro, quem esquecer o expurgo deixa registros excluídos acumulando para sempre, inclusive
|
|
26
|
+
dado pessoal, sem que nada avise.
|
|
27
|
+
|
|
28
|
+
O que precisa ser verdadeiro:
|
|
29
|
+
|
|
30
|
+
- toda entidade com exclusão lógica tem um prazo de retenção declarado ou declara, com motivo, que
|
|
31
|
+
guarda para sempre;
|
|
32
|
+
- o expurgo existe, roda sozinho, só o sistema o executa e deixa rastro do que apagou;
|
|
33
|
+
- o prazo prometido é cumprido: nada expurga antes dele;
|
|
34
|
+
- a ordem do que se apaga e o que se preserva continuam sendo decisão do domínio, porque variam:
|
|
35
|
+
um fluxo leva junto as execuções, e um relatório precisa apagar conversas em outro serviço e
|
|
36
|
+
preservar o consumo de IA;
|
|
37
|
+
- a falta de declaração quebra a verificação, em vez de depender de alguém lembrar.
|
|
38
|
+
|
|
39
|
+
## Decisão
|
|
40
|
+
|
|
41
|
+
### 1. A retenção é declarada na entidade
|
|
42
|
+
|
|
43
|
+
`softDelete` passa a aceitar um objeto, numa de duas formas mutuamente exclusivas:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
type SoftDelete =
|
|
47
|
+
| true // aceito pelo tipo; reprovado pelo `opus check` (seção 6)
|
|
48
|
+
| {
|
|
49
|
+
retainDays: number // inteiro positivo; conta a partir de `deletedAt`
|
|
50
|
+
timezone: string // IANA, obrigatório: a rotina não depende do fuso da máquina
|
|
51
|
+
label: string // título da action, completo: 'Expurgar relatórios excluídos'
|
|
52
|
+
description: string // descrição da action, completa
|
|
53
|
+
schedule?: { cron?: string; enabled?: boolean } // padrão: '30 3 * * *', habilitada
|
|
54
|
+
batchSize?: number // padrão: 200 linhas por lote; no máximo 10_000
|
|
55
|
+
maxDurationMs?: number // padrão: 60_000 por execução
|
|
56
|
+
}
|
|
57
|
+
| { retain: 'forever'; reason: string }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Toda forma diferente de `true` mantém a exclusão lógica. Os pontos do pacote que hoje testam
|
|
61
|
+
`softDelete === true` passam por uma única função, `hasSoftDelete(entity)`, que reconhece as três:
|
|
62
|
+
`entityColumns` (injeção de `deletedAt`), o tipo `SoftDeletePart` e o `kyselyRepo` (filtro de
|
|
63
|
+
leitura e exclusão lógica). Sem isso, a forma de objeto deixaria de injetar a coluna e de filtrar,
|
|
64
|
+
sem erro nenhum.
|
|
65
|
+
|
|
66
|
+
### 2. Uma entidade com retenção pertence a exatamente um domínio
|
|
67
|
+
|
|
68
|
+
A action e a rotina do expurgo nascem no domínio que lista a entidade em `entities`.
|
|
69
|
+
|
|
70
|
+
- A mesma entidade com retenção em dois domínios, ou num domínio e num subdomínio dele, falha com
|
|
71
|
+
`domain.entity_registered_twice`: no `validateDomain`, dentro da árvore de um domínio, e no
|
|
72
|
+
`register` do runtime, entre domínios registrados separadamente. A comparação é pela tabela, para
|
|
73
|
+
pegar também duas declarações `defineEntity` da mesma tabela, que gerariam dois expurgos dela.
|
|
74
|
+
- Entidade com `retainDays` que não aparece em nenhum `entities` de domínio é reprovada pelo
|
|
75
|
+
`opus check`. O check liga a entidade ao domínio pelo identificador exportado, como já liga um
|
|
76
|
+
`bindAction` ao seu contrato. Assim, uma entidade usada só por repositório não fica sem expurgo.
|
|
77
|
+
|
|
78
|
+
### 3. O que o Opus gera
|
|
79
|
+
|
|
80
|
+
`flattenDomain` deriva, de cada entidade com `retainDays`, um contrato e uma rotina, e a checagem de
|
|
81
|
+
duplicidade do `validateDomain` passa a incluí-los. A derivação é pura e isomorfa, como o resto do
|
|
82
|
+
`flattenDomain`.
|
|
83
|
+
|
|
84
|
+
**Nomes.** A action se chama `<caminho-do-domínio>.<entidade-em-kebab>.purge-deleted`. Quando a
|
|
85
|
+
entidade em kebab é igual ao último segmento do domínio, o segmento não se repete: a entidade
|
|
86
|
+
`automation` no domínio `automation` gera `automation.purge-deleted`, e a entidade `reportShare` no
|
|
87
|
+
domínio `report` gera `report.report-share.purge-deleted`. A rotina se chama `<action>.daily`. O
|
|
88
|
+
caminho do domínio entra como está, sem conversão, como nas demais actions do domínio.
|
|
89
|
+
|
|
90
|
+
**Contrato da action:**
|
|
91
|
+
|
|
92
|
+
- entrada `{ olderThanDays?: number }`: inteiro, no mínimo o `retainDays` e no máximo 3650. O
|
|
93
|
+
padrão é o `retainDays`. Expurgar antes do prazo é recusado, para cumprir a retenção declarada;
|
|
94
|
+
- saída `{ purged: number }`;
|
|
95
|
+
- `effect: 'write'`, `automatable: true`, `internal: true` (não fica exposta por REST),
|
|
96
|
+
`personalData: false` e `ai: false`. O motivo de ficar fora da IA (é operação do sistema, não
|
|
97
|
+
pedido de uma pessoa) fica registrado no fonte do SDK, porque a `description` é do consumidor;
|
|
98
|
+
- autorização: só o sistema (`isSystemActor`). Não há execução manual por pessoa nesta decisão;
|
|
99
|
+
- textos: o `label` e a `description` declarados na retenção, completos e literais. O Opus não
|
|
100
|
+
monta frase a partir do nome do recurso, porque um modelo fixo erra a concordância (um recurso
|
|
101
|
+
feminino viraria "conversas excluídos"). O `opus copy` passa a ler esses dois campos no
|
|
102
|
+
`defineEntity`, como já lê os textos de `defineContract`.
|
|
103
|
+
|
|
104
|
+
**Rotina:** `defineSchedule` com a action, `cron` (padrão `'30 3 * * *'`), o `timezone` declarado e
|
|
105
|
+
`enabled` (padrão verdadeiro). Ela roda como o sistema, conforme a ADR 0028. Como qualquer rotina,
|
|
106
|
+
exige um `SchedulerAdapter` no `start()`, mesmo com `enabled: false`.
|
|
107
|
+
|
|
108
|
+
### 4. Quem executa: handler padrão ou `bindPurge`
|
|
109
|
+
|
|
110
|
+
O contrato é gerado no core. O handler é anexado no servidor:
|
|
111
|
+
|
|
112
|
+
- **Padrão:** quando o runtime tem um `DataAdapter` Kysely, o `register` anexa o handler padrão.
|
|
113
|
+
Ele apaga as linhas com `deletedAt` anterior ao corte, das mais antigas para as mais novas, em
|
|
114
|
+
lotes de `batchSize`. Repete os lotes até não sobrar nada ou até esgotar `maxDurationMs`.
|
|
115
|
+
- **Próprio:** quando apagar a linha não basta, o domínio liga `bindPurge(entity, handler)` e o
|
|
116
|
+
registra em `purges` no `defineDomain`. `isDomainConfig` passa a reconhecer `purges`. O handler
|
|
117
|
+
recebe `{ ctx, cutoff, batchSize }`, apaga um lote e devolve `{ ids: string[] }`. O Opus repete os
|
|
118
|
+
lotes enquanto houver ids e tempo, e continua dono do contrato, da rotina, dos lotes e do registro.
|
|
119
|
+
Respeitar o `cutoff` é obrigação do handler: o Opus recusa um `olderThanDays` abaixo do prazo, mas
|
|
120
|
+
só o handler decide o que apaga. Um lote que devolve um id já removido na mesma execução, ou mais
|
|
121
|
+
ids que o `batchSize`, interrompe o expurgo com `purge.no_progress`; sem essa guarda, um handler
|
|
122
|
+
que não avança repetiria lotes até o fim do tempo. Antes de parar, o Opus registra os ids novos
|
|
123
|
+
daquele lote, que o handler já apagou. Entre um lote e outro, o Opus cede a vez ao event loop.
|
|
124
|
+
Ele decide a ordem, o que fica e o que sai em outro serviço. É a separação da ADR 0002: a entidade,
|
|
125
|
+
com a retenção, fica no contrato; o handler fica no servidor.
|
|
126
|
+
- **Sem handler padrão aplicável e sem `bindPurge`**, por exemplo sem `DataAdapter` ou com um que não
|
|
127
|
+
seja Kysely: o `start()` recusa com `runtime.purge_handler_missing`, nomeando a entidade.
|
|
128
|
+
- **No modo design**, a action gerada se comporta como as demais: entra no catálogo, e o handler é o
|
|
129
|
+
do design. As rotinas não são iniciadas.
|
|
130
|
+
|
|
131
|
+
**Dependências no banco:** o schema do Opus não declara cascata; ela depende da DDL do consumidor.
|
|
132
|
+
Se uma chave estrangeira restritiva impedir a exclusão, a execução agendada falha com o erro do
|
|
133
|
+
banco no log e na auditoria da execução, e nada é apagado daquele lote. Como a linha travada está
|
|
134
|
+
entre as mais antigas, ela volta ao primeiro lote em toda execução: o expurgo da entidade fica
|
|
135
|
+
parado até a chave ser ajustada. A saída é ajustar a chave, com cascata na DDL, ou usar `bindPurge`
|
|
136
|
+
para apagar os dependentes primeiro.
|
|
137
|
+
|
|
138
|
+
### 5. O registro do que saiu
|
|
139
|
+
|
|
140
|
+
Cada lote que apaga algo registra a chave `opus.purge`, com a entidade, a action, o corte, o
|
|
141
|
+
`olderThanDays`, o número do lote e os ids removidos, em blocos de no máximo 500 ids. Assim, um
|
|
142
|
+
acúmulo grande vira vários registros de tamanho limitado, nunca uma linha sem teto. No fim da
|
|
143
|
+
execução que apagou algo, `opus.purge.total` traz a quantidade, o número de lotes e se restou
|
|
144
|
+
trabalho: verdadeiro quando a execução parou por tempo com lote cheio ou foi interrompida por erro.
|
|
145
|
+
Os registros vão para o `ctx.log` quando existe e, senão, para o logger do runtime. Os únicos rastros
|
|
146
|
+
de um expurgo são esses registros e a auditoria da execução agendada.
|
|
147
|
+
|
|
148
|
+
### 6. O `opus check` cobra a regra
|
|
149
|
+
|
|
150
|
+
A regra `soft-delete-retention` reprova:
|
|
151
|
+
|
|
152
|
+
- entidade com `softDelete: true`, sem retenção;
|
|
153
|
+
- `retain: 'forever'` sem `reason`;
|
|
154
|
+
- `retainDays` sem `timezone`, `label` ou `description`, ou com valor que não é inteiro positivo
|
|
155
|
+
ou passa de 3650, e `batchSize` que não é inteiro positivo ou passa de 10.000;
|
|
156
|
+
- entidade com `retainDays` fora de qualquer `entities` de domínio;
|
|
157
|
+
- `bindPurge` para uma entidade sem `retainDays`;
|
|
158
|
+
- `bindPurge` exportado que não aparece no `purges` de nenhum domínio, porque cairia em silêncio no
|
|
159
|
+
handler padrão.
|
|
160
|
+
|
|
161
|
+
### 7. Documentação
|
|
162
|
+
|
|
163
|
+
`docs/protocol.md` e `docs/data-layer.md` passam a mostrar a forma com retenção e deixam de
|
|
164
|
+
apresentar `softDelete: true` como suficiente. O trecho de `protocol.md` que promete uma "opção
|
|
165
|
+
explícita" para incluir excluídos na leitura é corrigido: essa opção não existe hoje.
|
|
166
|
+
|
|
167
|
+
### 8. O que fica fora
|
|
168
|
+
|
|
169
|
+
- Restaurar um registro excluído dentro do prazo. É útil, mas independente do expurgo, e entra
|
|
170
|
+
quando houver o caso.
|
|
171
|
+
- Execução manual do expurgo por uma pessoa.
|
|
172
|
+
- Um motor de retenção para o que não é entidade com exclusão lógica, como logs e trilhas de
|
|
173
|
+
auditoria. Eles têm política própria.
|
|
174
|
+
- A introspecção estática usada pelo MCP de desenvolvimento ainda não mostra a action e a rotina
|
|
175
|
+
geradas; o manifest mostra.
|
|
176
|
+
|
|
177
|
+
## Consequências
|
|
178
|
+
|
|
179
|
+
- Quem usa `softDelete: true` precisa declarar a retenção ou o motivo para guardar. O `opus check`
|
|
180
|
+
passa a cobrar isso, então a versão é major, com a migração no changelog.
|
|
181
|
+
- O expurgo deixa de ser código repetido em cada domínio. O terceiro caso já nasce com prazo,
|
|
182
|
+
rotina, lotes, autorização e registro, e o esquecimento quebra a verificação.
|
|
183
|
+
- A action de expurgo aparece no manifest e na documentação, e a rotina na lista de agendamentos.
|
|
184
|
+
Fica visível o que cada projeto apaga e quando.
|
|
185
|
+
- O expurgo é irreversível: depois dele, restam só o registro `opus.purge` e a auditoria.
|
|
186
|
+
|
|
187
|
+
## Alternativas consideradas
|
|
188
|
+
|
|
189
|
+
- **Manter o expurgo no consumidor, com uma convenção escrita.** É o estado de hoje. Não há
|
|
190
|
+
garantia: o próximo domínio pode esquecer, e nada avisa.
|
|
191
|
+
- **Contrato e teste de gate só no consumidor.** Resolveria a Grand Brasil, mas deixaria o
|
|
192
|
+
padrão fora do lugar onde a exclusão lógica já mora, e cada projeto repetiria o gate.
|
|
193
|
+
- **Expurgo implícito, sem declarar prazo.** Um padrão único, por exemplo 30 dias, para toda
|
|
194
|
+
entidade com `softDelete`. Esconderia uma decisão de retenção, que pode envolver dado pessoal,
|
|
195
|
+
atrás de um padrão silencioso.
|
|
196
|
+
- **Fuso padrão do projeto em vez de fuso por entidade.** Seria menos repetitivo, mas o pacote não
|
|
197
|
+
tem esse padrão hoje, e o check, que é estático, não conseguiria verificá-lo. Fica para quando
|
|
198
|
+
houver uma configuração de projeto para agendamentos.
|
|
199
|
+
- **Um motor genérico de retenção para qualquer tabela.** Cobriria logs e trilhas, mas mistura
|
|
200
|
+
políticas diferentes e é cedo demais com os casos que existem.
|
|
201
|
+
|
|
202
|
+
## Verificação
|
|
203
|
+
|
|
204
|
+
- Testes do SDK:
|
|
205
|
+
- a forma de objeto mantém a exclusão lógica: injeta `deletedAt` e o `kyselyRepo` filtra e marca;
|
|
206
|
+
- a entidade com `retainDays` gera a action e a rotina com os nomes da seção 3, incluindo o caso de
|
|
207
|
+
entidade em kebab diferente do domínio, e com as declarações de contrato e autorização;
|
|
208
|
+
- entrada abaixo do `retainDays` é recusada, e chamador que não é o sistema também;
|
|
209
|
+
- o handler padrão apaga só os excluídos antes do corte, em lotes, do mais antigo para o mais novo,
|
|
210
|
+
para no tempo e registra `opus.purge` com os ids, em blocos limitados, e `opus.purge.total`;
|
|
211
|
+
- uma linha restaurada ou excluída de novo entre a seleção e a exclusão não é apagada;
|
|
212
|
+
- um handler que repete ids ou excede o `batchSize` falha com `purge.no_progress`, rápido e sem
|
|
213
|
+
inflar o registro, e os ids novos do lote que parou entram no registro e no total;
|
|
214
|
+
- o `bindPurge` substitui o padrão e mantém contrato, lotes e registro;
|
|
215
|
+
- entidade em dois domínios, ou duas entidades da mesma tabela, falham com
|
|
216
|
+
`domain.entity_registered_twice`;
|
|
217
|
+
- o `start()` sem handler falha com `runtime.purge_handler_missing`;
|
|
218
|
+
- chave estrangeira restritiva faz a execução falhar sem apagar o lote, e a seguinte também;
|
|
219
|
+
- o core (`src/core/index.ts`) continua sem pacote externo em tempo de execução: os schemas da
|
|
220
|
+
action gerada são Standard Schema com o JSON Schema anexado, sem zod.
|
|
221
|
+
- Testes do `opus check`: os seis casos da seção 6 reprovam, e a declaração completa passa.
|
|
222
|
+
- Teste do `opus copy`: o `label` e a `description` da retenção entram no inventário.
|
|
223
|
+
- Na Grand Brasil, a adoção migra os fluxos de automação para `softDelete` com retenção e cria a
|
|
224
|
+
exclusão lógica dos relatórios com expurgo próprio.
|