Fluxyr

Fluxyr Research · Geração de código

Quill: Aplicações Web como Micro-Prompts

Uma prova de conceito para geração de código efêmero — uma spec, três linguagens.

Em Geração de Código Efêmero argumentamos que o ativo durável em um sistema construído por IA não é o código — é a especificação. Código gerado para fazer um trabalho pode ser descartado e regenerado sob demanda; a spec compacta em linguagem natural, o micro-prompt, é o que merece ser versionado e mantido.

Aquele artigo terminava com uma conjectura: uma ferramenta que versiona um projeto como uma sequência de micro-prompts e um estado resolvido, e regenera o código de fato sob demanda — do jeito que migrações de banco versionam um schema. O Quill é essa ferramenta, construída como prova de conceito. Este post relata o que ele é e o mostra funcionando: a mesma especificação, regenerada em uma aplicação Python/Flask, uma TypeScript/Express e uma Ruby/Sinatra — cada uma produzindo comportamento idêntico byte a byte.

O Quill é open source (github.com/fluxyr-app/quill) e está no npm (npm i -g @fluxyr/quill).

A ideia: código como artefato derivado

Uma aplicação web convencional é uma base de código que você mantém. Uma aplicação Quill é uma pilha ordenada de micro-prompts que você mantém; o código em generated/ é um artefato derivado — como um binário compilado, ou um schema de banco materializado. Você edita os prompts, não o código, e o Quill regenera.

Três camadas fazem isso funcionar:

  • Convenções centrais — uma especificação única, compartilhada e independente de linguagem de como uma aplicação se comporta: como funcionam os identificadores, o que é um recurso, como os endpoints são moldados, como funcionam autorização e erros. São de responsabilidade do Quill e reutilizadas por todos os alvos. O ponto crucial: elas não contêm código nem framework — apenas comportamento.
  • Bindings de stack — um stack mapeia essas convenções em um framework. Cada stack traz um binding.md (a realização no framework), um scaffold mínimo e um passo verify() (compilar / lint / testar). O Quill traz três: Flask, Express, Sinatra.
  • Prompts de funcionalidade — os micro-prompts do seu próprio projeto, aplicados em ordem como migrações. 0001-create-users.md, 0002-add-projects.md, e assim por diante.

A separação é o truque todo. Aqui está a especificação inteira do esquema de identificadores — uma convenção central, compartilhada literalmente entre as três linguagens:

prompts / core / identifiers
# Identifiers — short IDs

Every resource is identified by a UUID. Internally the full UUID is used.
Externally — everywhere an id crosses the boundary to a client — the id is
represented as a compact short ID.

- The short ID is a Base-62 encoding of the 128-bit UUID using the alphabet
  0-9, then A-Z, then a-z, most-significant digit first, left-padded to
  exactly 22 characters.
- Encoding and decoding are exact inverses: any UUID -> short ID -> UUID
  returns the original UUID.
- Ids are never hand-formatted in output; serializing a resource emits its
  short ID automatically. Inbound ids are resolved to UUIDs before use.

Nada ali é Python, TypeScript ou Ruby. O binding de cada stack diz como realizá-la — "implemente em app/utils/uuid.py", "em src/utils/shortId.ts", "em lib/utils/short_id.rb" — e o modelo escreve o codec de fato.

O motor

quill up aplica os prompts de funcionalidade pendentes. Para cada um:

  1. Compor o system prompt: as convenções centrais compartilhadas + o binding do stack.
  2. Gerar — um LLM (via OpenRouter, então qualquer modelo funciona) retorna edições de arquivo inteiro como um pequeno protocolo JSON; o Quill as aplica.
  3. Auto-revisão — uma passada em que o modelo critica a própria saída contra a spec.
  4. Verificar — o verify() do stack roda a toolchain real (pytest / tsc + node:test / rake test). Em caso de falha, os erros são devolvidos por até N passadas de reparo.
  5. Registrar — o hash do prompt, o hash do código resultante e o modelo são gravados em quill.lock.json, o livro-razão do estado resolvido.

Este é o "laço de reparo" do artigo original, tornado concreto: um modelo barato mais uma passada de auto-revisão, com gate de feedback de execução.

Um passo a passo

terminal
$ npm install -g @fluxyr/quill
$ export OPENROUTER_API_KEY=sk-or-...

$ create-quill-app blog --stack sinatra
✓ Created Quill project 'blog' (stack: sinatra, model: minimax/minimax-m3)

$ cd blog
$ quill new "create users"
✓ created prompts/0001-create-users.md

Você preenche prompts/0001-create-users.md com uma spec — note que ela não nomeia arquivos nem Ruby:

prompts / 0001-create-users
Add a User resource.

Fields:
- email — string, required, unique.
- name — string, required.

Endpoints (account-scoped): list; create (400 if email/name missing, 409 if
the email already exists); get one by id (404 if missing).

Tests: cover create + list + get, and assert the returned id is a 22-character
short ID.

Então quill up. A execução transmite o progresso — incluindo uma fase … thinking para modelos de raciocínio, então nunca parece travado:

terminal — quill up
$ quill up
Applying 2 pending prompt(s) via OpenRouter (model: anthropic/claude-haiku-4.5)...

▸ applying 0001-create-users
  → anthropic/claude-haiku-4.5 …
    … receiving (3035 chars, 8s)
  · 4 file(s) written (13s) — User resource: short-ID codec, model,
    account-scoped list/create/get routes, tests.
  ↻ self-review pass
  ✓ verify: syntax + tests pass
✓ 0001-create-users  (repair passes: 1)

▸ applying 0002-add-projects
  → anthropic/claude-haiku-4.5 …
  · 3 file(s) written (11s) — Project model, projects routes
    (list/create/get/delete), tests.
  ↻ self-review pass
  ✓ verify: syntax + tests pass
✓ 0002-add-projects  (repair passes: 1)

✓ Up to date — applied 2 prompt(s).

O diretório generated/ agora contém uma aplicação Sinatra funcional. quill status mostra o livro-razão; quill checkpoint v1 tira um snapshot e prova a reprodutibilidade regenerando do zero e re-rodando os testes.

Funciona de verdade?

Três sinais independentes dizem que sim.

O laço de reparo pega bugs reais. Não são hipotéticos. Na aplicação Flask, a auto-revisão pegou o modelo alucinando uma chave estrangeira para uma tabela que não existia e a removeu. Em uma execução posterior, a auto-revisão de um modelo de raciocínio encontrou e corrigiu um bug de estreitamento de null estrito em TypeScript na própria saída:

self-review note
· Fixed strict-null-check issue: the resolved userUuid (string | null) was used
  after a truthy check on a different variable, so it wasn't narrowed. Restructured
  to check userUuid directly before use.

verify() é um gate rígido. Um prompt só é marcado como aplicado depois que a toolchain real passa — pytest no Flask, tsc --noEmit + node --test no Express, rake test no Sinatra. Código gerado que não compila ou falha nos testes dispara outra passada de reparo, em vez de ser aceito em silêncio.

A regeneração é reprodutível. quill checkpoint tira um snapshot da aplicação, depois faz uma regeneração completa a partir da pilha de prompts e a verifica. Com um modelo determinístico a temperatura 0, duas regenerações independentes da mesma aplicação produziram código idêntico byte a byte (batendo com o codeHash no livro-razão) — os prompts realmente reproduzem a aplicação, não apenas algo parecido.

Uma spec, três linguagens

Esta é a demonstração central. Escrevemos dois prompts de funcionalidade — um recurso User e um recurso Project pertencente a um usuário — e os aplicamos, sem mudar a intenção, nos três stacks. A convenção de identificadores é o comportamento compartilhado mais fundamental, então examinamos de perto o codec de short-ID gerado em cada linguagem.

Python (Flask)app/utils/uuid.py gerado:

python
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

class Uuid:
    @staticmethod
    def shorten(value):
        num = value.int if isinstance(value, uuid.UUID) else uuid.UUID(value).int
        chars = []
        for _ in range(22):
            num, rem = divmod(num, 62)
            chars.append(ALPHABET[rem])
        return "".join(reversed(chars))

TypeScript (Express)src/utils/shortId.ts gerado:

typescript
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

export function shorten(uuid: string): string {
  let num = BigInt("0x" + uuid.replace(/-/g, ""));
  const digits: string[] = [];
  while (num > 0n) {
    digits.unshift(ALPHABET[Number(num % 62n)]);
    num = num / 62n;
  }
  return digits.join("").padStart(22, "0");
}

Ruby (Sinatra)lib/utils/short_id.rb gerado:

ruby
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

def self.shorten(uuid)
  num = uuid.to_s.delete("-").to_i(16)
  encoded = ""
  while num > 0
    encoded = ALPHABET[num % 62] + encoded
    num /= 62
  end
  encoded.rjust(22, "0")
end

Três linguagens, três idiomas (o divmod do Python, o BigInt do TypeScript, o to_i(16) do Ruby), um algoritmo. Para confirmar que não são apenas parecidos, mas equivalentes, cruzamos cada codec gerado contra uma implementação de referência do esquema Base-62 em mais de 2.000 UUIDs, incluindo os casos-limite (tudo-zero, tudo-um):

Os três codecs são idênticos byte a byte à referência em mais de 2.002 UUIDs, com round-trips perfeitos.

E as aplicações se comportam de forma idêntica na rede. Uma requisição ao vivo contra cada uma — criar um usuário, criar um projeto referenciando-o, buscá-lo, tentar uma duplicata — retorna os mesmos códigos de status e o mesmo formato de short-ID de 22 caracteres:

live round-trip — identical across stacks
user 201 · id length 22 · project 201 · FK matches · get 200 · duplicate email 409

Os números

StackLinguagem / frameworkModeloArquivos (2 features)TestesCodec vs. referência
FlaskPython / Flask / SQLAlchemyminimax-m3618 passam (pytest)idêntico / 2.003 UUIDs
ExpressTypeScript / Expressclaude-haiku-4.5811 passam (node:test) + tsc limpoidêntico / 2.002 UUIDs
SinatraRuby / Sinatraclaude-haiku-4.5718 execuções, 58 asserções (rake)idêntico / 2.002 UUIDs

Os mesmos dois prompts, três stacks, um comportamento.

Os mesmos dois prompts, três linguagens, um comportamento. A spec é o ativo; a linguagem é uma escolha de deploy.

O que aprendemos

Um modelo barato mais uma passada de auto-revisão bastam. Coerente com o benchmark original, não precisamos de um modelo de fronteira. Tanto modelos rápidos e baratos quanto um modelo de raciocínio mais lento chegaram a código aprovado e verificado — a auto-revisão e o gate de verificação fazem o trabalho pesado, não a capacidade bruta do modelo.

Um novo stack é o melhor teste da sua spec. Construir o terceiro stack foi onde as convenções foram estressadas. O Express registra routers na raiz da aplicação, então as rotas precisam de caminhos completos — mas o modelo, razoavelmente, assumiu um mount com prefixo e escreveu caminhos relativos, gerando 404s. A correção não estava no modelo nem no código; foi tornar o binding explícito ("declare caminhos completos e absolutos") com um exemplo trabalhado. Uma convenção subespecificada é invisível até que uma segunda implementação discorde da primeira.

Modelos de raciocínio precisam de uma fase de pensamento visível. Um modelo transmitiu vários minutos de tokens de raciocínio interno antes de emitir qualquer código. Como nosso display de progresso inicialmente só mostrava conteúdo, a execução parecia travada. Mostrar o fluxo de raciocínio como um heartbeat … thinking resolveu a UX — uma lição pequena, mas real, sobre conduzir modelos de raciocínio em uma ferramenta.

Higiene de contexto importa mais do que você imagina. Um bug inicial deixou o coletor de contexto entrar em node_modules, inflando um único prompt para mais de 200 mil tokens. Um modelo barato tolerou em silêncio (lenta e caramente); um modelo com limite rígido de contexto rejeitou de imediato. O gerador precisa excluir agressivamente o diretório de dependências de cada stack.

O código ainda importa — só que não do jeito que importava

É tentador ler "código é um artefato derivado" como "código é descartável". É forte demais. O código gerado ainda roda em produção, ainda tem superfície de segurança, ainda acumula características de desempenho e dívida técnica. Ele precisa morar em algum lugar — commitado, deployado, observável. O que muda não é se o código importa, mas como nos relacionamos com ele.

Hoje, a relação humana com o código é revisão: alguém lê um pull request de 25.000 linhas e tenta raciocinar sobre sua correção. Isso não escala — e num mundo de geração por IA é a camada errada para revisar de qualquer forma. O diff é uma renderização de uma decisão que foi de fato tomada na spec.

O formato plausível é uma divisão de trabalho:

  • Humanos revisam especificações. O micro-prompt é o artefato pequeno, durável e significativo — é o que é lido, discutido e versionado. Uma spec de 30 linhas é revisável de um jeito que um diff de 25 mil linhas nunca foi.
  • Agentes cuidam do código. O código armazenado é trabalhado continuamente por agentes autônomos cujo trabalho não é adicionar funcionalidades — funcionalidades vêm da spec — mas melhorar o que já existe: reduzir dívida, apertar o desempenho, corrigir vulnerabilidades, refatorar em direção às convenções, manter as dependências atualizadas. Eles operam sobre uma base de código armazenada e real, mas não estão presos a um humano lendo um diff gigante.

Nesse mundo, o livro-razão do Quill — prompts, mais código resolvido, mais estado de verificação — é o substrato contra o qual as duas metades trabalham. Um humano muda uma spec e o código regenera; um agente melhora o código e o gate de verificação mais a spec o mantêm honesto. Ninguém revisa um PR de 25 mil linhas, porque esse nunca foi o artefato que valia a pena revisar.

O Quill hoje implementa só a primeira metade — da spec ao código. A segunda metade — frotas de agentes que cuidam continuamente do código armazenado contra uma spec e um gate de verificação — é o próximo sistema natural a construir. O código não desaparece. Ele apenas deixa de ser aquilo que os humanos esmiúçam e passa a ser aquilo que os agentes mantêm.

Limitações e próximos passos

O Quill é uma prova de conceito, e honesta sobre isso. A geração não é byte-determinística em geral (temperatura 0 ajuda, mas não garante entre modelos); o laço de reparo interno do checkpoint é hoje mais fino que o interativo; e os stacks entregues usam armazenamentos em memória ou SQLite para ficarem herméticos, não datastores de produção. A economia é atraente, mas real — cada quill up é um punhado de chamadas ao modelo.

A direção, porém, é clara. As convenções centrais já são portáveis entre três linguagens; adicionar uma quarta (Go, FastAPI, Rails) é um binding.md e um scaffold, não uma reescrita. E a afirmação mais profunda — de que uma pilha versionada de especificações é o programa, e o código é uma saída de build — agora tem uma implementação funcional por trás.

Experimente

terminal
npm install -g @fluxyr/quill
export OPENROUTER_API_KEY=sk-or-...

create-quill-app my-app --stack flask   # or node / sinatra
cd my-app
quill new "create users"
quill up

Fonte: github.com/fluxyr-app/quill.

O Quill foi construído e validado em uma única sessão de trabalho. Cada transcrição, codec e número deste artigo vêm dessa execução.