@agility-luhn/compiler
v2.9.1
Published
Compiler and generators for the LUHN domain DSL.
Readme
@luhn/compiler
Compilador da DSL LUHN: lê arquivos .luhn, valida semanticamente, expõe AST resolvida e emite artefatos para
TypeScript (interfaces + Zod + service + controller), SQL (DDL Postgres), OpenAPI 3.1 e um índice canônico de metadados.
Pacote independente e publicavel no npm. Os artefatos de distribuicao ficam em
dist/(main: "dist/index.js",types: "dist/index.d.ts").
1. Sumário das Capacidades
| Capacidade | Implementação |
|---------------------------------------------|---------------|
| Parser de .luhn | src/lexer.ts, src/parser.ts |
| Análise semântica e resolução de símbolos | src/semantic.ts |
| Loader de arquivos por application+module | src/file-loader.ts |
| API principal (compile, tipos) | src/compiler.ts, src/index.ts |
| Mini-linguagem implementation script | src/script.ts |
| Watcher com debounce | src/watcher.ts |
| Geração TypeScript (model/service/controller/Zod) | src/generator/typescript-generator.ts |
| Geração SQL (Postgres) | src/generator/sql-generator.ts |
| Geração de schema canônico do banco | src/generator/database-schema-generator.ts |a
| Geração OpenAPI 3.1 | src/generator/openapi-generator.ts |
| Geração de metadata leve | src/generator/metadata-generator.ts |
| Mapa canônico (introspecção em runtime) | src/generator/canonical-map-generator.ts |
| Validadores/fills e máscaras Delphi em type/campo | src/parser.ts, src/semantic.ts, src/generator/canonical-map-generator.ts |
| Configuration declarativo (configuration) com AST dedicada e geração orientada a business | src/ast.ts, src/parser.ts, src/semantic.ts |
| Validação Zod isolada de controller | src/controller-zod.ts |
| Protocolo de plugins (extensões em build) | src/plugin.ts |
2. API Pública
import { compile, CompileOptions, CompileResult } from '@luhn/compiler';
const result = await compile('./luhn', {
setup, // LuhnGenerationSetup (multitenancy, runtime importPath, driver SQL)
typescript: true, // gera *.interface.ts, *.schema.ts, *.model.ts, *.handle.ts
sql: true, // gera generated/database/schema.sql por modulo
openapi: true, // gera generated/openapi/openapi.json por modulo
metadata: true, // gera generated/metadata/metadata.json por modulo
outputDir: './src/luhn'
});
if (result.diagnostics.some(d => d.severity === 'error')) {
for (const d of result.diagnostics) console.error(d.code, d.message);
}CompileResult expõe:
program— AST bruta.resolved— AST apósresolveProgram(símbolos resolvidos, tipos normalizados, mixins aplicados).diagnostics—{ code, severity, message, span: { filePath, startLine, startColumn } }[].artifacts— mapapath -> conteúdode todos os arquivos emitidos antes da gravação.
Diretivas auxiliares (re-exportadas por src/index.ts): parseProgram, resolveProgram,
loadApplicationInputs, createWatcher, compileScript, geradores individuais e tipos
(Program, ResolvedProgram, LuhnGenerationSetup, Diagnostic, Span).
3. Linguagem LUHN (estado atual)
3.1 Declarações top-level
application, module, import, enum, type, record, business, configuration, mixin, service, library.
Regras estruturais aplicadas pelo semantic:
- cada entrada da aplicacao hospedeira ou de plugin pertence a uma
application. - cada
modulepertence a exatamente umaapplication; duas applications nao podem compartilhar modulo ou schema normalizado. - toda declaração precisa pertencer a um
module. application.modules: [...]deve bater com osmodule <nome>declarados (mão dupla).- ciclo entre módulos é erro.
importprecisa resolver para símbolo ou módulo existente.- import entre módulos exige
depends on <application>no módulo consumidor. - import entre applications exige também
exported trueno módulo provedor. - referências por FQCN em campo/parametro/mixin target são proibidas — exige
import+ nome curto. - mixin com colisão de campos → erro.
defaultprecisa ser literal compatível com o tipo (enum aceita só membro válido).service implementation scriptexige corpo.
3.1.1 Extensão entre applications
Tipos que formam contrato público devem ser declarados em um módulo exportado. Tipos globais da application não são uma API entre plugins.
application reguarda
version "2.0.0"
module catalogo
owner true
exported true
application fiscal
version "1.0.0"
module tributacao
owner true
depends on reguardaO módulo consumidor pode importar todo o módulo público pelo namespace application.modulo. Os tipos e
businesses desse módulo ficam disponíveis por nome curto:
import reguarda.catalogo
mixin produto
ncm: ncm "NCM"Para evitar colisões ou limitar o contrato, use o import seletivo application.modulo.simbolo com alias. O
mesmo mecanismo vale para tipos em campos e para o alvo de mixin:
import reguarda.catalogo.produto as Produto
import reguarda.catalogo.origemProduto as OrigemProduto
type ncm string 8
mixin Produto
ncm: ncm "NCM"
origemFiscal: OrigemProduto "Origem fiscal"depends on sempre usa a application provedora (depends on reguarda). Imports sempre declaram ao menos
application.modulo; a forma modulo.simbolo não é aceita.
Para integração opcional, mantenha o núcleo independente e publique um plugin adaptador que dependa das duas
aplicações. Um mixin nunca é aplicado silenciosamente quando seu alvo está ausente.
3.1.2 Configuration (configuration)
configuration e uma declaracao de alto nivel para configuracoes operacionais vinculadas a um business,
mas no backend de geracao reaproveita a mesma semantica de persistencia/CRUD de business.
Diretrizes de uso:
- Nome no formato
business.nomeConfig(sem FQCN completo no corpo, resolucao porimport). @scopesegue as regras existentes (global,conta,unidade) e aproveita o setup de multitenancy.- O AST preserva
kind = configpara diferenciar no semantic/geradores. - A estrategia de persistencia pode ser especializada por gerador (ex.: tabela unificada de configuracoes por modulo) sem perder o contrato de CRUD/OData do runtime.
Exemplo:
import exchange.message
@summary "Roteamento por tipo de mensagem"
@scope conta
configuration message.routeByType
messageType: notificationCode
channel: channel
provider: provider
active: boolean = true
priority: smallint = 03.2 Tipos primitivos reconhecidos
uuid, string, text, number, smallint, float, money, boolean, date, timestamp, json, xml,
url, image, crypto, vector, file, storage, any.
O resolver normaliza primitivos em TS (string/number/boolean/unknown) e gera os tipos físicos
apropriados no SQL e no OpenAPI.
3.3 Campos e parâmetros
nome?: tipo[] = valor "Descricao" preferred?opcional.[]array (gerachild_tableno SQL salvo embedded_record).- default literal (string, number, boolean, ref a enum).
- string final = descrição de exibição.
preferredmarca o campo "principal" para apresentação.validators.<funcao>associa validador declarativo aotypealias ou ao campo.fills.<funcao>(campo1,campo2,...)associa preenchimento automático declarativo.= (expressao)marca o campo como calculado.- campos calculados aceitam aritmética
+ - * /, caminho de campo (a.b) e funçõessum,avg,min,max,coalesce. - o valor calculado é numérico no runtime (entrada textual numérica é convertida; vazio/null vira
0). - string com tokens Delphi (
0,9,L,l,A,a,C,c,#) define máscara de input. - expressão booleana entre parênteses no fim do campo controla visibilidade condicional.
Exemplos:
type cpf string 11 validators.cpf
record pessoa
documento: string 20 validators.cnpj
record endereco
cep: string 9 fills.cep(logradouro,bairro,cidade,estado) "00000-000"
record item
quantidade: number
preco: money
total: money = (preco * quantidade)
business comercial.saida "Saidas"
produtos: item[]
total: money = (sum(produtos.total))
record cliente
tipoPessoa: tipoPessoa
cpf?: cpf (tipoPessoa == FISICA)
cnpj?: cnpj (tipoPessoa == JURIDICA)Precedência de validação:
- se o campo declara
validators.<funcao>, ele prevalece. - se o campo não declara, herda validação do
typealias (quando houver).
Precedência de fill:
- se o campo declara
fills.<funcao>(...), ele prevalece. - se o campo não declara, herda fill do
typealias (quando houver).
Campos calculados: funções e BNF
Funções suportadas:
sum(caminho)soma os valores resolvidos no caminho (inclui coleção).avg(caminho)média aritmética dos valores resolvidos.min(caminho)menor valor numérico do caminho.max(caminho)maior valor numérico do caminho.coalesce(a, b, c, ...)retorna o primeiro argumento não nulo/vazio.
BNF específica de expressão calculada:
<calculated-clause> ::= "=" "(" <calc-expr> ")"
<calc-expr> ::= <calc-term> (("+" | "-") <calc-term>)*
<calc-term> ::= <calc-unary> (("*" | "/") <calc-unary>)*
<calc-unary> ::= ("+" | "-") <calc-unary> | <calc-primary>
<calc-primary> ::= NUMBER
| <path>
| "(" <calc-expr> ")"
| <calc-func>
<calc-func> ::= ("sum" | "avg" | "min" | "max") "(" <path> ")"
| "coalesce" "(" <calc-args>? ")"
<calc-args> ::= <calc-expr> ("," <calc-expr>)*
<path> ::= IDENT ("." IDENT)*3.4 Business e records
@scope conta
business financeiro.contaReceber "Contas a Receber"
numero: string
cliente: clientebusinessrecebe campo sintéticoid(UUID, roleprimary_key).- Records 1-nível sem arrays são candidatos a
embedded_record(achatado em colunas prefixadas no pai). - Records com arrays caem para
TEXT JSON. - Arrays de records → tabela filha com
parent_id+item_index.
3.5 Services
service modulo.nome "Label"
description "..."
input
campo: tipo
output
campo: tipo
throws
CODIGO "Mensagem"
implementation code # handler TS escrito a mão
# ou
implementation script # mini-linguagem transpilada
...3.6 Libraries
library modulo.nome "Label"
function nome
input
output3.7 Validadores e fills declarativos
O parser aceita validators.<funcao> como sufixo de type alias escalar e de campo.
Também aceita fills.<funcao>(campo1,campo2,...) como sufixo de type alias escalar e de campo.
O semantic resolve a referência para uma library e função reais e valida:
- library inexistente (
SEM019) - função inexistente na library (
SEM020) - ambiguidade de resolução da library (
SEM021)
O semantic resolve referências de fill e valida:
- library inexistente (
SEM022) - função inexistente na library (
SEM023) - ambiguidade de resolução da library (
SEM024)
Após resolução, o compilador propaga para o modelo resolvido:
validate?: { library: string; fn: string }
fill?: { library: string; fn: string; targets: readonly string[] }
mask?: string
visibleWhen?: stringEssa metadata segue para o objects-map.json e é consumida no runtime/UI para:
- validação assíncrona no client (blur);
- preenchimento automático no client (blur);
- validação server-side no CRUD antes da persistência.
Diagrama do fluxo:
flowchart LR
A[LUHN: type/campo com validators.funcao e fills.funcao] --> B[Parser]
B --> C[Semantic resolve library + funcao]
C --> D{Valido?}
D -- nao --> E[Diagnostics SEM019-SEM024]
D -- sim --> F[ResolvedField/ResolvedType com validate fill mask]
F --> G[CanonicalMapGenerator]
G --> H[objects-map.json com field.validate field.fill field.mask field.visibleWhen]
H --> I[Runtime Describe]
I --> J[UI: async validator e autofill no blur]
H --> K[Runtime CRUD create/update/patch]3.8 Máscaras no formato Delphi
Quando o parser encontra um sufixo STRING contendo tokens de máscara Delphi,
o valor é tratado como máscara do campo/tipo (sem @mask e sem keyword extra).
Exemplo:
type cpf string 14 validators.cpf "000.000.000-00"
record endereco
cep: string 9 fills.cep(logradouro,bairro,cidade,estado) "00000-000"Tokens suportados:
9: número opcional0: número obrigatórioL: letra obrigatórial: letra opcionalA: letra ou númeroa: letra ou número opcionalC: qualquer caracterec: qualquer caractere opcional#: número com sinal (+/-)- literais fixos:
:,/,-,.(e demais símbolos do template)
Diagrama do fluxo de máscara:
flowchart LR
A[Campo com string de mascara Delphi] --> B[Parser detecta mask template]
B --> C[Semantic aplica heranca de alias quando necessario]
C --> D[ResolvedField ou ResolvedType com mask]
D --> E[CanonicalMapGenerator]
E --> F[field.mask no objects-map]
F --> G[Runtime describe]
G --> H[UI aplica formatacao no input]3.9 Mini-linguagem implementation script
Statements: let, return, throw CODIGO, log, call modulo.library.funcao [with expr],
if (inline), for ... from ... to ... (inline ou bloco), push array expr, expression statement.
Expressões: literais, acesso qualificado a.b.c, métodos obj.metodo(arg), arrays e object literals
(suportam multiline via balanceamento), operadores aritméticos + - * /, comparação == != < <= > >=,
lógicos and or not, call como expressão (transpilada com await). Comentários // e #.
Validações: throw CODIGO só aceita códigos declarados no bloco throws. Transpila para TypeScript
chamando defineService(...) do runtime.
3.10 Gramática (BNF)
BNF simplificada da linguagem LUHN (baseada em src/lexer.ts + src/parser.ts).
Observações léxicas relevantes:
- blocos usam
INDENT/DEDENT(não usa{}); - comentários de linha:
// ...e# ...; - strings usam aspas duplas;
ScriptBodyé token especial capturado apósimplementation script.
<program> ::= <statement>* EOF
<statement> ::= <annotations>? (
<application>
| <import>
| <enum>
| <type-alias>
| <record>
| <business>
| <mixin>
| <service>
| <library>
)
<annotations> ::= <annotation>+
<annotation> ::= "@" <qualified-name> <annotation-value>?
<annotation-value> ::= STRING | NUMBER | BOOLEAN | <qualified-name>
<application> ::= "application" IDENT <application-block>?
<application-block> ::= INDENT <application-item>* DEDENT
<application-item> ::=
"title" STRING
| "description" STRING
| "version" STRING
| "database" IDENT
| "types" STRING
| "modules" <qualified-name-list>
| <application-module-decl>
<application-module-decl> ::= "module" <qualified-name> <module-block>?
<module-block> ::= INDENT <module-item>* DEDENT
<module-item> ::=
"title" STRING
| "owner" BOOLEAN
| "exported" BOOLEAN
| "enabled" BOOLEAN
| "types" STRING
| "depends" "on" <qualified-name-list>
<import> ::= "import" <qualified-name> ("as" IDENT)?
<enum> ::= "enum" IDENT INDENT <enum-entry>+ DEDENT
<enum-entry> ::= NUMBER ":" IDENT
<type-alias> ::= "type" IDENT (
INDENT <field>* DEDENT
| <type-ref> NUMBER? <type-alias-suffix>*
)
<type-alias-suffix> ::= "preferred" | <validator-ref> | <fill-ref> | <mask-template>
<record> ::= "record" IDENT STRING? <fields-block>?
<business> ::= "business" IDENT STRING? <fields-block>?
<mixin> ::= "mixin" <qualified-name> <fields-block>?
<fields-block> ::= INDENT <field>* DEDENT
<service> ::= "service" <service-name> STRING? <service-block>?
<service-name> ::= IDENT | IDENT "." IDENT
<service-block> ::= INDENT <service-item>* DEDENT
<service-item> ::=
"describe"
| "description" STRING
| "input" <parameters-block>
| "output" <parameters-block>
| "throws" <throws-block>
| "implementation" ("code" | "script" ScriptBody?)
<library> ::= "library" IDENT STRING? <library-block>?
<library-block> ::= INDENT <library-item>* DEDENT
<library-item> ::= "description" STRING | <library-function>
<library-function> ::= <annotations>? "function" IDENT <library-function-block>?
<library-function-block> ::= INDENT <library-function-item>* DEDENT
<library-function-item> ::=
"description" STRING
| "input" <parameters-block>
| "output" <parameters-block>
<parameters-block> ::= INDENT <parameter>* DEDENT
<parameter> ::= <annotations>? IDENT "?"? ":" <type-ref> <string-size>? <array-suffix>? <default-clause>?
<throws-block> ::= INDENT <throw-item>* DEDENT
<throw-item> ::= IDENT STRING?
<field> ::= <annotations>? IDENT "?"? ":" <type-ref> <array-suffix>? <string-size>? (<default-clause> | <calculated-clause>)? <field-suffix>*
<field-suffix> ::= STRING | "preferred" | <validator-ref> | <fill-ref> | <mask-template> | <visibility-expr>
<validator-ref> ::= "validators" "." IDENT
<fill-ref> ::= "fills" "." IDENT "(" <identifier-list> ")"
<mask-template> ::= STRING ; quando STRING contiver tokens Delphi de máscara
<visibility-expr> ::= "(" <boolean-expr> ")"
<boolean-expr> ::= <boolean-term> ("or" <boolean-term>)*
<boolean-term> ::= <boolean-factor> ("and" <boolean-factor>)*
<boolean-factor> ::= "not" <boolean-factor> | "(" <boolean-expr> ")" | <comparison>
<comparison> ::= IDENT "==" IDENT | IDENT "==" "true" | IDENT "==" "false" | IDENT
<type-ref> ::= IDENT ("." IDENT)*
<qualified-name> ::= IDENT ("." IDENT)*
<qualified-name-list> ::= <qualified-name> ("," <qualified-name>)*
<identifier-list> ::= IDENT ("," IDENT)*
<array-suffix> ::= "[" "]"
<string-size> ::= NUMBER
<default-clause> ::= "=" <literal>
<calculated-clause> ::= "=" "(" <calc-expr> ")"
<literal> ::= STRING | NUMBER | BOOLEAN | <qualified-name>
<calc-expr> ::= <calc-term> (("+" | "-") <calc-term>)*
<calc-term> ::= <calc-unary> (("*" | "/") <calc-unary>)*
<calc-unary> ::= ("+" | "-") <calc-unary> | <calc-primary>
<calc-primary> ::= NUMBER | <qualified-name> | "(" <calc-expr> ")" | <calc-func>
<calc-func> ::= ("sum" | "avg" | "min" | "max") "(" <qualified-name> ")"
| "coalesce" "(" (<calc-expr> ("," <calc-expr>)*)? ")"
BOOLEAN ::= "true" | "false"
IDENT ::= /[A-Za-z_][A-Za-z0-9_]*/
NUMBER ::= /-?[0-9]+(\.[0-9]+)?/Recorte BNF específico de fills e máscara
Para deixar o recurso de autofill explícito na gramática, este é o recorte mínimo:
<type-alias-suffix> ::= "preferred" | <validator-ref> | <fill-ref> | <mask-template>
<field-suffix> ::= STRING | "preferred" | <validator-ref> | <fill-ref> | <mask-template> | <visibility-expr>
<fill-ref> ::= "fills" "." IDENT "(" <fill-target-list> ")"
<fill-target-list> ::= IDENT ("," IDENT)*
<mask-template> ::= STRING ; tokens Delphi: 9 0 L l A a C c #
<visibility-expr> ::= "(" <boolean-expr> ")"Diagrama dedicado do fluxo de fills no compilador:
flowchart LR
A[DSL: fills.funcao targets] --> B[Parser reconhece fill-ref]
B --> C[Semantic resolve library e funcao]
C --> D{Referencia valida?}
D -- nao --> E[SEM022 SEM023 SEM024]
D -- sim --> F[ResolvedField ou ResolvedType com fill]
F --> G[CanonicalMapGenerator]
G --> H[field.fill com library fn targets]Diagrama estrutural da gramática:
flowchart TD
P[program] --> S[statement*]
S --> A[application]
S --> I[import]
S --> E[enum]
S --> T[type-alias]
S --> R[record]
S --> B[business]
S --> M[mixin]
S --> SV[service]
S --> L[library]
T --> TR[type-ref]
T --> F[field*]
T --> TS[type-alias-suffix*]
TS --> PREF[preferred]
TS --> VR[validator-ref]
R --> FB[fields-block]
B --> FB
M --> FB
FB --> F
F --> TR
F --> ARR[array-suffix?]
F --> SZ[string-size?]
F --> DEF[default-clause?]
F --> FS[field-suffix*]
FS --> DISP[display string]
FS --> PREF
FS --> VR
FS --> FR[fill-ref]
SV --> SB[service-block]
SB --> DESC[describe?]
SB --> SD[description?]
SB --> IN[input parameters]
SB --> OUT[output parameters]
SB --> THR[throws]
SB --> IMPL[implementation code ou script]
L --> LB[library-block]
LB --> LD[description?]
LB --> LF[library-function*]
LF --> IN
LF --> OUT
VR --> VLIB[validators]
VR --> VFN[function name]
FR --> FLIB[fills]
FR --> FFN[function name]
FR --> FTG[target fields]Primitivos reconhecidos em <type-ref> (quando não resolvidos como tipo nomeado):
uuid,string,text,number,smallint,float,money,boolean,date,timestamp,json,xml,url,image,crypto,vector,file,storage,any.
4. Geradores
4.1 TypeScriptGenerator
Por entidade ou business:
*.interface.ts— interface TS pura.*.enum.ts— uniões de string.*.schema.ts— schema Zod (validação de entrada).*.model.ts— DTO + helpers de conversão snake_case ↔ camelCase.
O CRUD HTTP genérico (insert/update/delete/get/list/
queryOData) vive 100% no runtime, empackages/runtime/src/http. Personalizações por entidade são feitas via hooksbefore/afterregistrados emcreateLuhnServer({ hooks })ou peloPluginInstallContext.registerHook(...). Não se gera mais*.service.tsnem*.controller.ts.
Por service:
services/<service>.handle.ts— descritor (defineService) tipado pelo input/output declarados.services/<service>.script.ts— quandoimplementation script, contém o handler transpilado.
Por library:
libraries/<library>.handle.ts.
Barrels:
services-scripts.ts— importa todos os.script.tspara auto-registro no boot.
Recursos adicionais:
- síntese de relações de multitenancy via
setup.multitenancy. - conversão automática de relações em colunas
*_idno service SQL. - ordenação determinística dos campos (
id, tenancy, demais alfabéticos). exactOptionalPropertyTypesrespeitado: opcionais saem com| undefinedexplícito.
4.2 SqlGenerator (Postgres)
Por módulo, emite generated/database/schema.sql:
- usa o schema da application proprietaria do modulo (por exemplo,
billingAppvirabilling_app). CREATE TABLEpor entidade/business.- FKs para relações detectadas.
- Tabelas filhas para arrays (records ou escalares) com
parent_id+item_index. - Defaults SQL para literais.
CHECKpara enums.- Índices automáticos em FKs.
- Flatten de aliases estruturados em colunas.
- Computed columns quando
calculated+ expressão suportada. gen_random_uuid()aplicado apenas em colunas comrole: 'primary_key';relation_keyem PK composta (ex.:pedido_venda_itens.pedido_venda_id) não recebe default.
Estratégias de armazenamento (storageStrategy):
| Estratégia | Quando | Resultado físico |
|-------------------|--------------------------------------------|------------------|
| column | escalar simples | coluna direta |
| relation_fk | referência a outra entidade | *_id + FK |
| child_table | array de records ou escalares | tabela filha |
| embedded_record | record 1-nível sem arrays | colunas prefixadas (endereco_logradouro, endereco_cidade, ...) |
Drivers configuráveis: 'postgres' (completo) e fallback 'generic'.
4.3 OpenApiGenerator
Por módulo, emite generated/openapi/openapi.json com:
- schemas de entities, enums, records e aliases estruturados.
- paths CRUD por recurso (GET list, GET id, POST, PUT, PATCH, DELETE).
- paths de services (
POST /api/<modulo>/<service>). - componentes de input/output de service.
- extensões
x-luhn-service,x-luhn-application,x-luhn-module. - tags por entidade e por grupo de services do módulo.
- exemplos OData prontos por entidade (com base em campos reais).
4.4 SchemaGenerator e CanonicalMap
DatabaseSchemaGeneratorproduz um JSON canônico das tabelas (consumido por@agility-solucoes/luhn-databaseno diff/migrate).- o metadata de banco
2.0registra application/schema, ownership de modulo e tipo fisico de enum explicitamente. - entidades, views, enums e tabelas auxiliares usam o schema da application; FKs podem atravessar schemas.
- metadata
1.1nao e compativel. Ao atualizar, remova os artefatos antigos e recrie o banco antes do proximo sync. CanonicalMapGeneratoremiteobjects-map.jsonpor módulo: mapa de FQCN → metadados resolvidos. É lido em runtime para introspecção (MCP, OpenAPI servido, etc.).- campos com validador são emitidos com
field.validate = { library, fn }no mapa canônico. - campos com autofill são emitidos com
field.fill = { library, fn, targets }no mapa canônico. - campos com máscara são emitidos com
field.mask = "template"no mapa canônico.
4.5 MetadataGenerator
JSON leve por módulo (application, module, lista de entities com campos básicos). Usado por dashboards.
5. Loader de Arquivos
loadApplicationInputs(dir):
- procura arquivo com
applicationno nome (fallback: primeiro.luhnda raiz). - lista recursivamente todos os
.luhn. - extrai módulos do
applicationpormodules: [...]ou por declaraçõesmodule <nome>. - filtra arquivos por nome de módulo no path.
- injeta automaticamente
module <pasta-raiz>em arquivos sem declaração explícita.
6. Watcher
createWatcher({ dir, onRecompile, ... }):
- observa
.luhnrecursivamente. - debounce 100ms.
- recompila e dispara
onRecompile(result). - em erro de FS/compile, devolve diagnostic
WATCH.
Usado pelo server-playground em --watch para recompilar + reregistrar services sem reiniciar o servidor.
7. Setup de Geração
LuhnGenerationSetup é o ponto de extensão do consumidor:
const setup: LuhnGenerationSetup = {
database: {
driver: 'postgres' // ou 'generic'
},
runtime: {
importPath: '@agility-solucoes/luhn-runtime' // ou './src' em dev
},
multitenancy: {
// opcional: ambos os blocos são injetados conforme @scope da business
conta: { fieldName: 'conta', typeFqcn: 'plataforma.conta', keyFieldName: 'id' },
unidade: { fieldName: 'unidade', typeFqcn: 'plataforma.unidade', keyFieldName: 'id' }
}
};Escopo das business
O conceito antigo de entity foi removido. Toda declaração de domínio é uma business, e o escopo de multitenancy é declarado por anotação:
| Anotação | Comportamento |
| ------------------- | -------------------------------------------------------------------------------- |
| @scope unidade | Padrão. Injeta conta e unidade (entidade isolada por unidade da conta). |
| @scope conta | Injeta apenas conta (compartilhado entre unidades da mesma conta). |
| @scope global | Não injeta nada (ex.: plataforma.conta, entidades do IAM). |
Exemplo:
@scope conta
business cliente "Clientes"
nome: string 150
documento: string 20Importante: o pacote IAM não define multitenancy própria — todas as suas businesses usam
@scope global. Quem decide o setup é o app hospedeiro.
8. Protocolo de Plugins do Compilador
src/plugin.ts define a forma como pacotes externos (ex.: luhn-iam) podem contribuir com
diretórios .luhn adicionais para a compilação do app. O runtime, ao iniciar o servidor, agrega os luhnDirs
de todos os plugins ativos e os passa ao compilador como inputs combinados.
Cada plugin conectado e uma application LUHN independente. O plugin declara applicationName, inclui um
application.luhn com seus modulos e recebe um schema PostgreSQL derivado desse nome. A application hospedeira
continua proprietaria apenas dos seus próprios modulos.
9. Diagnósticos
Cada Diagnostic traz:
{
code: string; // ex.: 'SEM001', 'PARSE042', 'TS_GEN'
severity: 'error' | 'warning';
message: string;
span: { filePath: string; startLine: number; startColumn: number; endLine: number; endColumn: number };
}Categorias principais: LEX, PARSE, SEM (semantic), TS_GEN, SQL_GEN, OPENAPI_GEN, SCRIPT, WATCH.
10. Scripts de Build
pnpm --filter @luhn/compiler build # tsc -p tsconfig.json11. Fluxo Completo do Compilador LUHN
Esta secao resume o pipeline real do compilador, da leitura dos arquivos .luhn ate a geracao dos artefatos consumidos pelo runtime, pelo banco e pelas UIs.
11.1 Visao de ponta a ponta
Pipeline executado por compile(dir, options):
loadApplicationInputs(dir)localiza o arquivo deapplication, descobre os modulos declarados, carrega os arquivos.luhnrelevantes e injeta entradas auxiliares detypesquando declaradas.parseProgram(inputs)tokeniza cada arquivo com o lexer, constroi a AST (ProgramNode) e acumula diagnosticos lexicos e sintaticos.resolveProgram(program)executa a analise semantica: resolve simbolos, imports, aliases, enum refs, records, relations, mixins, escopos, validadores, fills, mascaras e expressoes calculadas.compileInternal(...)monta oGenerationContextcomresolved,diagnosticsconsolidados esetupopcional.- Os geradores habilitados percorrem o
resolvede emitem artefatos em memoria. - O
CompileResultretornaprogram,resolved,diagnosticseartifacts; a gravacao fisica dos arquivos e responsabilidade da camada consumidora do compilador.
Arquivos principais envolvidos no fluxo:
src/compiler.ts- ponto de entrada assíncrono.src/file-loader.ts- descoberta e carga dos arquivos da aplicacao.src/lexer.ts- tokenizacao e controle deINDENT/DEDENT.src/parser.ts- AST e regras sintaticas.src/semantic.ts- resolucao semantica e validacoes estruturais.src/compiler-internal.ts- orquestracao da compilacao e dos geradores.src/generator/*- emissao dos artefatos finais.
11.2 Fases detalhadas
Fase 1. Descoberta e carga de arquivos
Responsavel: loadApplicationInputs(dir).
O loader:
- encontra o arquivo principal da aplicacao;
- extrai os modulos declarados em
application.modulesou nos blocosmodule; - lista recursivamente todos os
.luhndo diretorio; - filtra apenas os arquivos relevantes para os modulos ativos;
- carrega arquivos auxiliares declarados em
types "..."; - devolve
SourceFileInput[]compathrelativo etextUTF-8.
Essa fase define o universo de entrada da compilacao. Se um arquivo nao entrar aqui, ele nao participa da AST nem da geracao.
Fase 2. Analise lexica
Responsavel: tokenize(filePath, source) em src/lexer.ts.
O lexer:
- normaliza quebras de linha;
- converte texto em tokens (
Identifier,String,Number, palavras reservadas e simbolos); - controla blocos por indentacao com tokens
INDENTeDEDENT; - captura
ScriptBodyaposimplementation script; - gera diagnosticos como string nao terminada, caractere inesperado e uso invalido de
{/}.
Resultado da fase:
- stream de tokens por arquivo;
- diagnosticos
LEX*.
Fase 3. Analise sintatica
Responsavel: parseProgram(inputs) em src/parser.ts.
O parser:
- consome os tokens produzidos pelo lexer;
- monta a AST bruta da linguagem (
ProgramNode); - reconhece declaracoes top-level como
application,module,import,enum,type,record,business,configuration,mixin,serviceelibrary; - preserva spans para diagnosticos e tooling;
- interpreta campos, parametros, annotations, defaults, arrays, calculados, validators, fills, mascaras e
implementation script.
Resultado da fase:
- AST bruta (
program); - diagnosticos
PARSE*.
Fase 4. Analise semantica e resolucao
Responsavel: resolveProgram(program) em src/semantic.ts.
A analise semantica transforma a AST bruta em um modelo resolvido e consistente. Ela:
- resolve imports e simbolos referenciados;
- valida a unicidade e a coerencia de
applicationemodule; - detecta ciclos entre modulos;
- normaliza tipos primitivos e aliases;
- expande e valida mixins;
- identifica relacoes entre entidades;
- resolve
@scopee regras de multitenancy; - valida defaults, enums e tipos de campo;
- resolve
validators.<fn>efills.<fn>contralibrary.function; - propaga metadata como
validate,fill,maskevisibleWhen; - resolve expressoes calculadas e funcoes agregadoras suportadas.
Resultado da fase:
resolved(ResolvedProgram);- diagnosticos
SEM*; - IR pronta para geracao.
Fase 5. Montagem do contexto de geracao
Responsavel: compileInternal(inputs, options) em src/compiler-internal.ts.
Nessa fase o compilador:
- combina diagnosticos do parser com diagnosticos do semantic;
- injeta
setupopcional (database,runtime,multitenancy); - decide quais geradores vao rodar com base em
CompileOptions.
Selecao atual de geradores:
TypeScriptGeneratorOpenApiGeneratorDatabaseSchemaGenerator(habilitado quandodatabaseSchema !== falseousql)MetadataGeneratorCanonicalMapGenerator
Fase 6. Geracao de artefatos
Cada gerador recebe o mesmo GenerationContext e produz GeneratedArtifact[].
Artefatos principais:
- TypeScript: interfaces, enums, schemas Zod, models, handles de services e libraries.
- OpenAPI:
openapi.jsonpor modulo. - Database schema: JSON canonico do banco.
- SQL/Postgres: DDL derivada do modelo resolvido.
- Metadata: resumo leve por modulo.
- Canonical map:
objects-map.jsonpara introspeccao e runtime.
Observacao importante:
DatabaseSchemaGeneratorreaproveita o mapa canonico para consolidar objetos, enums e relacionamentos antes de montar a visao do banco.- o compilador monta os artefatos em memoria; quem grava em disco ou publica no runtime e a camada chamadora.
11.3 Palavras reservadas da linguagem
As palavras reservadas abaixo sao reconhecidas diretamente pelo lexer atual (KEYWORDS em src/lexer.ts). Elas nao devem ser usadas como identificadores soltos quando o contexto esperar nome de simbolo.
Top-level e estrutura:
applicationmoduleimportenumtyperecordbusinessconfigurationmixinservicelibraryfunction
Metadados e organizacao:
titleownerenableddependsonasdescriptiondescribeversiondatabasemodulestypes
Campos, parametros e comportamento:
preferredcalculatedinputoutputthrowsimplementationcodescript
Literais booleanos:
truefalse
Observacoes praticas:
validatorsefillsnao aparecem como palavras reservadas do lexer; elas entram como identificadores qualificados interpretados pelo parser/semantic.- funcoes de calculo como
sum,avg,min,maxecoalescetambem nao sao keywords lexicas; sao reconhecidas no parse de expressoes calculadas.
11.4 Diagrama do fluxo completo
flowchart TD
A[Diretorio da aplicacao LUHN] --> B[loadApplicationInputs]
B --> C[SourceFileInput[]]
C --> D[tokenize em src/lexer.ts]
D --> E[Tokens + diagnostics LEX]
E --> F[parseProgram em src/parser.ts]
F --> G[ProgramNode AST + diagnostics PARSE]
G --> H[resolveProgram em src/semantic.ts]
H --> I[ResolvedProgram + diagnostics SEM]
I --> J[compileInternal]
J --> K[GenerationContext]
K --> L[TypeScriptGenerator]
K --> M[OpenApiGenerator]
K --> N[DatabaseSchemaGenerator]
K --> O[MetadataGenerator]
K --> P[CanonicalMapGenerator]
L --> Q[Artefatos TS]
M --> R[openapi.json]
N --> S[schema.json e/ou SQL]
O --> T[metadata.json]
P --> U[objects-map.json]
Q --> V[CompileResult.artifacts]
R --> V
S --> V
T --> V
U --> V11.5 Fluxo resumido de analise ate geracao de codigo
Em uma frase: o compilador carrega os arquivos da aplicacao, tokeniza o texto, monta a AST, resolve semanticamente o dominio, consolida diagnosticos e so entao gera artefatos tipados e canônicos para TypeScript, OpenAPI, banco, metadata e runtime.
Resumo operacional:
- Entrada: arquivos
.luhneCompileOptions. - Analise lexica: tokens e erros de formato.
- Analise sintatica: AST completa.
- Analise semantica: resolucao de simbolos, tipos, relations, mixins e regras.
- IR resolvida: estrutura pronta para geracao.
- Geracao: artefatos derivados por modulo e por entidade.
- Saida:
CompileResultcom AST, IR resolvida, diagnosticos e artefatos.
Como main: "src/index.ts" e type: "commonjs", o build com tsc produz .js+.d.ts ao lado do .ts
(sem outDir). Em dev, os consumidores podem importar direto o .ts via ts-node.
11. Limitações Atuais
- Não há REPL nem CLI próprio (use
apps/cliou os scripts doapps/playground). - A mini-linguagem de
implementation scripté intencionalmente restrita — para lógicas complexas, useimplementation code(handler TS). - Generators emitem para Postgres; outros bancos caem no fallback
'generic'(sem FK/CHECK/índices). - Watcher é por polling do
fs.watchnativo do Node — pode perder eventos em filesystems remotos.
