@loom-forge/check 0.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/README.md +444 -0
- package/dist/index.d.ts +478 -0
- package/dist/index.js +867 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# @loom-forge/check
|
|
2
|
+
|
|
3
|
+
A camada de **TIPOS**. Dev/CI, **nunca no hot path de render** (ADR-206). Prova compatibilidade
|
|
4
|
+
estrutural por `subtype` do TSLite — é type-check, não validação de valor (ajv/zod).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add -D @loom-forge/check
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Três superfícies, e elas respondem perguntas diferentes — duas precisam de dados, uma não.
|
|
11
|
+
|
|
12
|
+
| superfície | pergunta | precisa de dados? |
|
|
13
|
+
| --------------------------------------- | ---------------------------------------------------------------- | ----------------- |
|
|
14
|
+
| `checkTree` | as props RESOLVIDAS cabem no `descriptor.schema`? | sim |
|
|
15
|
+
| `checkComponent` / `checkExpression` | as EXPRESSÕES tipam — e o call-site passa o que o chamado exige? | não |
|
|
16
|
+
| `componentType` / `componentAssignable` | a ÁLGEBRA de tipo de um componente (a base dos slots tipados) | não |
|
|
17
|
+
|
|
18
|
+
## Com dados: `checkTree` — props resolvidas ≤ `descriptor.schema`
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createTsliteChecker, checkTree } from "@loom-forge/check";
|
|
22
|
+
|
|
23
|
+
const checker = createTsliteChecker();
|
|
24
|
+
checker.assignable({ name: "Ana" }, propsJsonSchema); // forma do valor ≤ tipo do schema?
|
|
25
|
+
checker.subtype(typeA, typeB); // A atribuível a B (Schema do TSLite)
|
|
26
|
+
checkTree(resolved, registry, checker); // props de cada nó ≤ descriptor.schema
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Membro | O quê |
|
|
30
|
+
| -------------------------------------------------------- | -------------------------------------------------------------- |
|
|
31
|
+
| `subtype(a, b)` | atribuibilidade estrutural (port `TypeChecker`) |
|
|
32
|
+
| `assignable(value, jsonSchema)` | `inferValue(value)` ≤ `fromJsonSchema(jsonSchema)` |
|
|
33
|
+
| `inferType` · `fromJsonSchema`/`toJsonSchema` · `format` | pontes e diagnóstico |
|
|
34
|
+
| `checkTree(node, registry, checker)` | walk + valida props vs `descriptor.schema` → `DataflowIssue[]` |
|
|
35
|
+
|
|
36
|
+
Roda sobre a árvore RESOLVIDA, então infere a forma dos valores que de fato chegaram — pega
|
|
37
|
+
inclusive o que veio de expressão. Em troca, precisa de dados (uma amostra, uma fixture).
|
|
38
|
+
|
|
39
|
+
## Sem dados: `checkComponent` — as EXPRESSÕES, estaticamente
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { buildEnv, checkComponent, checkExpression } from "@loom-forge/check";
|
|
43
|
+
|
|
44
|
+
const env = buildEnv(def); // as raízes: props, state (e `data`, se você declarar o tipo)
|
|
45
|
+
checkComponent(def, env); // → CheckIssue[]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Não se reimplementa typecheck: o componente é **baixado** num corpo de função TSL e o
|
|
49
|
+
`@tslite/checker` tipa. Daí saem de graça o scope-flow e a inferência — o tipo do item de um
|
|
50
|
+
`each` vem **inferido da coleção**, sem anotação nenhuma.
|
|
51
|
+
|
|
52
|
+
```jsonc
|
|
53
|
+
{
|
|
54
|
+
"each": "{{ props.itens }}",
|
|
55
|
+
"as": "item",
|
|
56
|
+
"props": { "n": "{{ item.nomee }}" },
|
|
57
|
+
}
|
|
58
|
+
// → no-such-member: Property 'nomee' does not exist on type '{ nome: string; ativo: boolean }'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| pega | exemplo |
|
|
62
|
+
| --------------------------------------- | -------------------------------------------------------- |
|
|
63
|
+
| campo inexistente numa raiz | `{{ props.tituloo }}` · `{{ state.abertoo }}` |
|
|
64
|
+
| membro inexistente no item de um `each` | `{{ item.nomee }}` — tipo inferido da coleção |
|
|
65
|
+
| nome sem raiz | `{{ titulo }}` → `unknown-name` (não há escopo achatado) |
|
|
66
|
+
| `when` que não rende `boolean` | `not-assignable` — em vez de um nó sempre-visível |
|
|
67
|
+
| método de protótipo | `{{ xs.length }}` → erro; a forma é `{{ length(xs) }}` |
|
|
68
|
+
|
|
69
|
+
Cada issue traz três níveis de proveniência, e eles são independentes:
|
|
70
|
+
|
|
71
|
+
| campo | o quê | quando existe |
|
|
72
|
+
| --------- | ---------------------------------------------------------------------- | ----------------------------------- |
|
|
73
|
+
| `path` | o campo, como LISTA — `[0, 1, "props", "children"]` | **sempre** |
|
|
74
|
+
| `address` | o mesmo, como o autor o lê — `#0.1.props.children`. Derivado de `path` | **sempre** |
|
|
75
|
+
| `source` | o texto do campo, como o autor o escreveu | quando o campo era TEXTO |
|
|
76
|
+
| `span` | `{ from, to }` dentro de `source` (a palavra) | idem, e se o nó acusado tem posição |
|
|
77
|
+
|
|
78
|
+
No `path`, índice e chave são distintos no tipo (`2` anda em array, `"2"` em objeto), e é ele
|
|
79
|
+
que um serviço de linguagem traduz para a coordenada do documento; o `address` é exibição, e
|
|
80
|
+
ninguém o lê de volta. Na árvore a raiz é o nó `0` (`#0.1…`); fora dela o caminho é o do slot
|
|
81
|
+
(`["updates", "somar", "set", "n"]`, `updates.somar.set.n`).
|
|
82
|
+
|
|
83
|
+
`source.slice(span.from, span.to)` é o trecho aceso — sem compensação nenhuma, inclusive numa
|
|
84
|
+
string mista: o span já leva o deslocamento da ilha dentro do texto, e é a forma do `Span` do
|
|
85
|
+
`@tslite/language-service`, que é o que uma superfície consome. Uma definição
|
|
86
|
+
**já compilada** também tipa (a ilha é decodificada em nós nossos), e aí o issue vem com endereço
|
|
87
|
+
e sem span: a posição não existe, e inventá-la seria pior que não tê-la.
|
|
88
|
+
|
|
89
|
+
**A string é derivada uma vez.** O check tipa a definição COMPILADA — a mesma que o bind executa
|
|
90
|
+
— e as ilhas em memória que a compilação guardou, com posição no texto do campo. Quem já compilou
|
|
91
|
+
(a bancada, um serviço de linguagem) passa o artefato, e nada é derivado de novo:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { compile } from "@loom-forge/tslite/compile";
|
|
95
|
+
import { compileDefinition } from "@loom-forge/forge";
|
|
96
|
+
|
|
97
|
+
const compiled = compileDefinition(def, compile); // { def, diagnostics, islands }
|
|
98
|
+
checkComponent(def, env, { compiled }); // só o que é de TIPO
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Os diagnósticos da compilação (sintaxe, ilha vazia, o que o profile do deploy recusa) são de quem
|
|
102
|
+
compilou: com `compiled`, o check não os repete. Sem `compiled`, ele compila — sem profile, porque o
|
|
103
|
+
eixo de sintaxe não é dele — e os reporta junto. Um campo que não compilou entra como ausência de
|
|
104
|
+
valor, não como o texto que ficou nele: tipá-lo seria uma cascata do erro que a compilação já
|
|
105
|
+
acusou.
|
|
106
|
+
|
|
107
|
+
**`checkExpression(source, env, path, { resultType, limits })`** é o cano por slot — o que um
|
|
108
|
+
drawer de editor liga quando o autor abre um campo. Roda por tecla digitada, daí os `limits`; e
|
|
109
|
+
sintaxe quebrada vira diagnóstico (`parse-error`), nunca exceção na cara de quem está digitando.
|
|
110
|
+
|
|
111
|
+
**`analyzeComponent(def, env, opts)`** é o `checkComponent` sem a projeção: devolve os `issues` e
|
|
112
|
+
o que os produziu — o `Lowered`, o `CheckResult` do corpo (o `overlay` de tipo e escopo por nó) e
|
|
113
|
+
o `Env` dos `init`. É o que um serviço de linguagem consome para responder o **escopo de um
|
|
114
|
+
campo** sem tipar de novo: o `Lowered.frames` diz, por nó (`#0.1`) e por update (`updates.x`), o
|
|
115
|
+
bloco do corpo baixado em que os slots daquele lugar foram emitidos, e o `scopeAt` do
|
|
116
|
+
`@tslite/checker` sobre esse bloco é o escopo — o item do `each`, os `params` — pela mesma
|
|
117
|
+
passada. É o que o `@loom-forge/language-service` faz.
|
|
118
|
+
|
|
119
|
+
`lowerComponent(def, { compiled })` é exportado para prova e tooling: devolve o `program` (o AST
|
|
120
|
+
baixado), as `anchors` (nó → caminho, por identidade), os `texts` (endereço → o texto do campo),
|
|
121
|
+
os `frames` e as `islands` (as raízes tipadas). Para LER o que está sendo tipado,
|
|
122
|
+
`print(program)` do `@tslite/printer` — imprimir é saída, nunca passo do pipeline: o corpo é
|
|
123
|
+
**construído** com builders e entregue direto ao checker, e é isso que faz a âncora funcionar por
|
|
124
|
+
identidade de nó (ADR-210, ADR-218, ADR-219).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## A fronteira do `unknown` — o asserter, não o cast
|
|
129
|
+
|
|
130
|
+
Não saber o tipo de algo **não é errado**; `any` é. O certo é `unknown`, e a única ponte de
|
|
131
|
+
`unknown` para tipado é o **asserter**: tipa em build e **prova em runtime**. `as` não existe
|
|
132
|
+
aqui — seria tipar sem prova —, e o checker o recusa: `{{ (props.bruto as Endereco).cep }}` é
|
|
133
|
+
`narrowing-cast-not-allowed` no campo, e `as any` é `any-not-allowed`. A régua vem do
|
|
134
|
+
`types.strict` do profile do deploy (`checkComponent(def, env, { profile })`, default o
|
|
135
|
+
`DEFAULT_PROFILE` do bind); do profile o `check` aplica só o eixo de tipos — o que a expressão pode
|
|
136
|
+
conter é acusado pela compilação (ADR-217). O veredito é o mesmo nos dois canos — o campo por
|
|
137
|
+
tecla (`checkExpression`) e o componente inteiro —, porque a ilha da definição compilada é o nó
|
|
138
|
+
como está, com o `as` aninhado dentro. Só um cast **na raiz** (`{{ props.x as T }}`) não chega a
|
|
139
|
+
tipo nenhum: a compilação o recusa como `island-not-a-value`, porque um cast não computa nada.
|
|
140
|
+
|
|
141
|
+
Passe o catálogo de tipos nomeados e ele nasce:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const Address = s.object({ cep: { type: s.string }, city: { type: s.string } });
|
|
145
|
+
const env = buildEnv(def, { types: { Address } });
|
|
146
|
+
|
|
147
|
+
// {{ parseAs(Address, props.bruto).city }} → string ✅
|
|
148
|
+
// {{ parseAs(Address, props.bruto).cidade }} → no-such-member ← o checker volta a morder
|
|
149
|
+
// {{ props.bruto.cidade }} → passa: `unknown` navega em silêncio
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Errar a ponte deixou de ser silêncio** (`@tslite/checker` ≥ 0.9): cada forma tem código no nó do
|
|
153
|
+
argumento — `unknown-name` (o nome não existe), `type-as-value`, `not-a-schema`,
|
|
154
|
+
`asserter-needs-name`, `missing-argument`. Um `as` que tome o nome de um tipo do catálogo é
|
|
155
|
+
`shadows-schema`, apontando o campo `as` — o checker o vê porque o lowering faz do `as` um
|
|
156
|
+
**parâmetro** do corpo baixado. E a prova que **só pode falhar** é acusada em build:
|
|
157
|
+
`parseAs(Address, { cep: 1 })` é `unprovable-value` quando o tipo do valor não se sobrepõe ao alvo.
|
|
158
|
+
|
|
159
|
+
**`parseAs`, e não `parse`:** a stdlib já tem um `parse` — o `JSON.parse`, `(text) => unknown`. O
|
|
160
|
+
nome é o `ASSERTER` que o `@tslite/validate` publica (reexportado como `DEFAULT_ASSERTER`), e um
|
|
161
|
+
tipo do catálogo com o nome de um operador ou de uma raiz é recusado na montagem do `env`
|
|
162
|
+
(`BoundaryError`) — erro de quem monta o sistema, não diagnóstico do programa. Sem `types`, não há
|
|
163
|
+
asserter no `env` e a guarda custa zero.
|
|
164
|
+
|
|
165
|
+
**A outra metade é do binder, e é do chamador.** Tipar é metade; provar é a outra. As duas saem do
|
|
166
|
+
MESMO catálogo, pelo `createBoundary` do `@tslite/validate`: a metade do checker é o que o
|
|
167
|
+
`buildEnv` monta por dentro; a de runtime, o host entrega ao binder:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { createBoundary } from "@tslite/validate";
|
|
171
|
+
import { vanilla } from "@tslite/operators";
|
|
172
|
+
|
|
173
|
+
const boundary = createBoundary({
|
|
174
|
+
types: { Address },
|
|
175
|
+
reserved: Object.keys(vanilla),
|
|
176
|
+
});
|
|
177
|
+
createTsliteBinder({ boundary }); // `parseAs(Address, x)` valida em runtime
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Não embutimos isso: quem não usa asserter não deve pagar o validador, e provisão é do host — a
|
|
181
|
+
mesma doutrina do `ActionPort`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## O call-site — `props passadas ≤ props declaradas`
|
|
186
|
+
|
|
187
|
+
A tese é "B exige/muda um param → A, que compõe B, quebra". Ligue o resolvedor e ela fecha:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { buildEnv, checkComponent, declaredPropsOf } from "@loom-forge/check";
|
|
191
|
+
|
|
192
|
+
checkComponent(def, buildEnv(def), { propsOf: declaredPropsOf(systemIR) });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
{ "type": "Greeting", "props": { "name": "{{ props.qtd }}" } }
|
|
197
|
+
// → not-assignable @ #0.props.name — `number` não cabe em `name: string`
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| o que passava como `any` | agora |
|
|
201
|
+
| -------------------------------------------------------- | ------------------------------------------ |
|
|
202
|
+
| prop vinda de `{{ }}` com o tipo errado | `not-assignable`, no CAMPO, com o span |
|
|
203
|
+
| a mesma prop dentro de um `each` (usando o item do laço) | idem — o escopo do laço existe no lowering |
|
|
204
|
+
| prop que o chamado NÃO declara | `excess-property`, ancorado na **chave** |
|
|
205
|
+
| prop obrigatória faltando | `not-assignable`, no OBJETO (é sobre ele) |
|
|
206
|
+
| a mesma coisa numa definição já COMPILADA | idem, com endereço e sem span |
|
|
207
|
+
|
|
208
|
+
**Sem `propsOf`, nenhum call-site é tipado** — e não "todos falham". Quem baixa o componente só
|
|
209
|
+
para ver o corpo não tem sistema para perguntar, e inventar um erro ali esconderia o que essa
|
|
210
|
+
pessoa foi olhar.
|
|
211
|
+
|
|
212
|
+
> [!WARNING]
|
|
213
|
+
> **Prop extra deixou de ser aceita.** Antes o call-site era provado por `subtype` sobre o objeto
|
|
214
|
+
> inteiro, e o subtyping estrutural aceita largura a mais. Agora é o motor que prova, com a regra
|
|
215
|
+
> que o `tsc` aplica a um literal de objeto: uma prop que o chamado não declara é `excess-property`.
|
|
216
|
+
> Num nocode ela é peso morto que o autor não consegue ver — o componente simplesmente a ignora.
|
|
217
|
+
|
|
218
|
+
**Varrer o sistema inteiro** é o laço, e ele é curto — cada componente tem o seu `env`:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
for (const def of registry.list())
|
|
222
|
+
issues.push(...checkComponent(def, buildEnv(def), { propsOf }));
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## As DECLARAÇÕES — um `TypeRef` que não resolve é erro, não `any`
|
|
228
|
+
|
|
229
|
+
```jsonc
|
|
230
|
+
{ "props": { "n": "numero" } }
|
|
231
|
+
// → unknown-type-name @ props.n
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Era o furo mais caro que restava: o shorthand com typo virava `any`, e `any` **desliga toda a
|
|
235
|
+
prova daquela prop** — call-site, membro, span. Um erro de digitação na declaração apagava a
|
|
236
|
+
máquina inteira sem dizer nada. Agora o que não resolve vira `unknown` (o programa se obriga a
|
|
237
|
+
lidar) e o `checkDeclarations` diz **qual nome** não existe, no endereço da declaração
|
|
238
|
+
(`props.n`, `state.aberto`).
|
|
239
|
+
|
|
240
|
+
Um nome do **catálogo** vale como `TypeRef`: com `types: { Address }`, `"props": { "addr":
|
|
241
|
+
"Address" }` resolve. Sem o catálogo, o mesmo nome é reportado — e isso é o certo, porque dali
|
|
242
|
+
ele de fato não existe.
|
|
243
|
+
|
|
244
|
+
### O `init` do estado — provado no escopo que SEMEIA
|
|
245
|
+
|
|
246
|
+
```jsonc
|
|
247
|
+
{
|
|
248
|
+
"state": {
|
|
249
|
+
"email": { "type": "string", "init": "{{ props.perfil.email }}" },
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
O `init` é expressão, avaliada **uma vez, no mount** — a semântica do `useState(props.x)`. Ele é
|
|
255
|
+
tipado num `Env` PRÓPRIO (`buildInitEnv`): só `props` e `ctx`, sem `state` (é o que está sendo
|
|
256
|
+
definido) e sem `params` (não há chamada). É a mesma projeção que o runtime passa ao `initState`,
|
|
257
|
+
e é por isso que ele não divide o env do corpo:
|
|
258
|
+
|
|
259
|
+
| escrito no `init` | o que acontece |
|
|
260
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
261
|
+
| `{{ props.inicial }}` | tipa contra o `type` declarado, no endereço `state.<k>.init` |
|
|
262
|
+
| `0` | constante atravessa intacta |
|
|
263
|
+
| `{{ state.valor }}` | `unknown-name` — em runtime ele nem existe ali |
|
|
264
|
+
|
|
265
|
+
> [!WARNING]
|
|
266
|
+
> Antes disto, o `init` **não passava por nada**: nem compilação, nem bind, nem typecheck. Um
|
|
267
|
+
> `"{{ props.x }}"` ali virava texto literal na tela, e `{ "type": "number", "init": "abc" }`
|
|
268
|
+
> passava. Os dois silêncios fecharam juntos.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Os UPDATES LOCAIS — o `set` é um call-site sobre o ESTADO
|
|
273
|
+
|
|
274
|
+
É a perna que fechava o anel: props entram, `state` é lido, e aqui ele é **escrito**.
|
|
275
|
+
|
|
276
|
+
```jsonc
|
|
277
|
+
{
|
|
278
|
+
"state": { "count": { "type": "number", "init": 0 } },
|
|
279
|
+
"updates": {
|
|
280
|
+
"add": {
|
|
281
|
+
"params": { "n": "number" },
|
|
282
|
+
"set": { "count": "{{ state.count + params.n }}" },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| erro | onde pousa |
|
|
289
|
+
| ---------------------------------------------- | ---------------------------------- |
|
|
290
|
+
| valor do tipo errado | `updates.add.set.count` |
|
|
291
|
+
| campo que o estado não declara | `updates.add.set.<chave>` |
|
|
292
|
+
| o evento passa um param do tipo errado | `#0.events.onClick.params.n` |
|
|
293
|
+
| o evento passa um param que a ação não declara | `#0.events.onClick.params.<chave>` |
|
|
294
|
+
|
|
295
|
+
O `set` é **parcial**: escrever um campo não obriga a escrever os outros.
|
|
296
|
+
|
|
297
|
+
**`params` é dado de FORA.** Sem `params` declarado ele vale `unknown` — e escrevê-lo direto num
|
|
298
|
+
estado tipado não passa. Não é aspereza: é a mesma fronteira das props, e declarar é a saída (e
|
|
299
|
+
de quebra tipa o call-site do evento que dispara a ação).
|
|
300
|
+
|
|
301
|
+
### A ação do HOST também é provada — passe a porta
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
checkComponent(def, buildEnv(def), { actions: port });
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
A assinatura vem do `paramsOf` do Tier 1 (o `ActionPort`, o commander, ou qualquer objeto com o
|
|
308
|
+
método), em JSON Schema — a borda que o `TypeRef` já atravessa. Com ela, `events.params` de uma
|
|
309
|
+
ação do host vira o mesmo call-site do update local: obrigatória faltando, tipo errado,
|
|
310
|
+
`excess-property` na chave. E a ordem de resolução é a do RUNTIME — **update local primeiro, host
|
|
311
|
+
depois** —, porque um componente com um update chamado `salvar` não alcança a action `salvar` da
|
|
312
|
+
aplicação, e provar contra ela apontaria para um contrato que aquele nome nem toca.
|
|
313
|
+
|
|
314
|
+
**Sem a porta, a ação do host não é tipada — e isso é silêncio, não erro.** Quem não publica
|
|
315
|
+
assinatura não afirmou nada. A porta tem a outra metade do contrato, o **`resultOf`** — a forma
|
|
316
|
+
do RETORNO —, e é ela que tipa o `event` de um `then` (abaixo).
|
|
317
|
+
|
|
318
|
+
### `resolve` — o que a camada de COMANDO preenche
|
|
319
|
+
|
|
320
|
+
```jsonc
|
|
321
|
+
{ "events": { "onClick": { "action": "lead/open", "resolve": ["id"] } } }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Um comando resolve param por token (`$focus`), por default, ou **perguntando** — abrindo um drawer,
|
|
325
|
+
um seletor. Isso é legítimo, e é diferente de esquecer. Então se declara:
|
|
326
|
+
|
|
327
|
+
| o param… | o que acontece |
|
|
328
|
+
| ------------------------------ | ------------------------------------- |
|
|
329
|
+
| foi passado | é provado contra o tipo |
|
|
330
|
+
| está em `resolve` | conta como preenchido, e não falta |
|
|
331
|
+
| não está em nenhum dos dois | **erro** — obrigatória faltando |
|
|
332
|
+
| está em `resolve` e não existe | `excess-property`, no campo `resolve` |
|
|
333
|
+
|
|
334
|
+
É o que impede o default virar sorte. Em runtime o `resolve` **não faz nada**: ele é declaração, e
|
|
335
|
+
quem preenche é o commander, com os resolvers dele.
|
|
336
|
+
|
|
337
|
+
### O nome de COMANDO — provado contra a ação que ele executa
|
|
338
|
+
|
|
339
|
+
```jsonc
|
|
340
|
+
{
|
|
341
|
+
"id": "s",
|
|
342
|
+
"type": "Section",
|
|
343
|
+
"commands": {
|
|
344
|
+
"abrir": { "action": "lead/open", "params": { "motivo": "atalho" } },
|
|
345
|
+
},
|
|
346
|
+
"children": [
|
|
347
|
+
{
|
|
348
|
+
"id": "b",
|
|
349
|
+
"type": "Button",
|
|
350
|
+
"events": { "onClick": { "action": "abrir" } },
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
O evento é provado contra a ação que o nome EXECUTA, nos degraus da expansão: update local,
|
|
357
|
+
vocabulário de onde o nó monta, porta (ADR-015 do repo). O que o alias já liga conta como
|
|
358
|
+
preenchido. E o próprio alias é provado onde foi escrito, contra a assinatura com tudo opcional,
|
|
359
|
+
porque ele é aplicação parcial: faltar é legítimo, sobrar e errar o tipo não são.
|
|
360
|
+
|
|
361
|
+
| escrito | onde pousa |
|
|
362
|
+
| ------------------------------------------------------- | ------------------------------------ |
|
|
363
|
+
| o alias liga uma chave com o tipo errado | `#0.commands.abrir.params.<chave>` |
|
|
364
|
+
| o alias liga uma chave que a ação não declara | idem, `excess-property` |
|
|
365
|
+
| o disparo não passa a obrigatória que o alias não ligou | `#0.0.events.onClick.params` |
|
|
366
|
+
| o disparo passa uma chave que a ação não declara | `#0.0.events.onClick.params.<chave>` |
|
|
367
|
+
|
|
368
|
+
Passe a montagem do sistema, e um filho de slot é provado pelo vocabulário da sessão onde monta:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { mountingOf } from "@loom-forge/forge";
|
|
372
|
+
|
|
373
|
+
checkComponent(def, buildEnv(def), {
|
|
374
|
+
actions: port,
|
|
375
|
+
mounting: mountingOf(sys.components.values()),
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Sem ela, o documento é o sistema:** o que ele declara resolve dentro dele, e o resto vale como o
|
|
380
|
+
id da porta. Com ela, um nome que algum vocabulário declara e o documento não resolve depende de
|
|
381
|
+
onde o componente monta — e não é provado contra assinatura nenhuma. Ver ADR-216.
|
|
382
|
+
|
|
383
|
+
### O `event` de uma intenção — a ENTRADA de dado, provada (ADR-018 do repo)
|
|
384
|
+
|
|
385
|
+
```jsonc
|
|
386
|
+
{
|
|
387
|
+
"events": {
|
|
388
|
+
"onChange": {
|
|
389
|
+
"action": "editar",
|
|
390
|
+
"params": { "campo": "cpf", "valor": "{{ event }}" },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Tudo o que uma intenção recebe entra pelo `params`, e `event` é a raiz que o carrega: o que o
|
|
397
|
+
widget emitiu, o `data` da ação anterior num `then`, o `error` num `catch`. O lowering abre um
|
|
398
|
+
frame por intenção com `event` como parâmetro **tipado**, e os `params` são tipados dentro dele:
|
|
399
|
+
|
|
400
|
+
| onde | o tipo de `event` vem de | sem declaração |
|
|
401
|
+
| -------------------------- | ---------------------------------------------------------------------------------- | -------------- |
|
|
402
|
+
| `events.onX` num widget | `descriptor.meta.emits.onX` (seam `emitsOf`) | `unknown` |
|
|
403
|
+
| `events.onX` num `el` | — | `unknown` |
|
|
404
|
+
| `then` de uma ação do host | `resultOf(action)` da porta (`actions`) | `unknown` |
|
|
405
|
+
| `then` de um update local | `undefined` — o estado foi escrito, e só | — |
|
|
406
|
+
| `catch` | `{ code: string; message?: string; data?: unknown }`, o contrato do `ActionResult` | — |
|
|
407
|
+
| um efeito (e o `cleanup`) | `undefined` — nada o emitiu; o `then`/`catch` dele seguem as linhas acima | — |
|
|
408
|
+
|
|
409
|
+
E **`unknown` num param tipado é `not-assignable`**: quem não declarou o que emite (o kit) ou o
|
|
410
|
+
que devolve (a porta) não ganha um tipo de graça — a leniência que o `as` tinha saiu com ele. O
|
|
411
|
+
autor sai disso declarando ou provando (`parseAs`). Cada elo de uma cadeia é um call-site (o
|
|
412
|
+
`params` contra a assinatura da ação que ele executa), e cada `then`/`catch` abre o próprio
|
|
413
|
+
`event`, que sombreia o de fora — como numa promise chain. Os caminhos seguem a cadeia:
|
|
414
|
+
`#0.events.onClick.then.params.valido`, `#0.events.onClick.catch.params.motivo` na árvore;
|
|
415
|
+
`effects.carregar.then.params.user`, `effects.carregar.cleanup.catch.params.motivo` fora dela — é
|
|
416
|
+
o mesmo caminho em que a compilação do `forge` os deixa (`componentSlotsOf`). Um efeito com
|
|
417
|
+
`then` é "carregar no mount e guardar o que carregou", e o `event` desse `then` é o `resultOf`
|
|
418
|
+
da ação, como num evento.
|
|
419
|
+
|
|
420
|
+
**`pending[ação]`** é raiz do `env` de um componente **vivo** (`updates` ou `effects`):
|
|
421
|
+
`Record<string, boolean>`, `true` do despacho ao resultado. Num componente puro é `unknown-name`,
|
|
422
|
+
e o remédio é o que o torna vivo.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## A álgebra de tipo de um componente
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import {
|
|
430
|
+
componentType,
|
|
431
|
+
componentAssignable,
|
|
432
|
+
declaredPropsOf,
|
|
433
|
+
} from "@loom-forge/check";
|
|
434
|
+
|
|
435
|
+
componentType(ir); // { props: Schema, category?, tags, entry }
|
|
436
|
+
componentAssignable(child, { props }); // child ≤ Component<C> no eixo de props
|
|
437
|
+
declaredPropsOf(systemIR); // o resolvedor que liga o call-site (acima)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`TypeRef` (o que a declaração escreve) resolve para `Schema` do TSLite, que é o SSOT: shorthand
|
|
441
|
+
(`"string"`), Schema literal, ou JSON Schema **só na borda** (via `@tslite/jsonschema`). Prop com
|
|
442
|
+
nome terminado em `?` é opcional, TS-like — e opcional aqui significa **`T | undefined`**, porque
|
|
443
|
+
props viajam como JSON e em JSON `undefined` não existe: `{}` e `{ body: undefined }` são o mesmo
|
|
444
|
+
documento (ADR-214).
|