# Morph.AiKit

Solução .NET **compilável** que prova os standards `ai-agents/**` contra
`framework/ai-pin.json`. Não é fragmento de referência: a unidade de consumo é o
diretório inteiro, e é por isso que ela está registrada no `REGISTRY.json` como
`kind: "project"` (uma entrada, `path: "dotnet/ai-kit/"`).

```
ai-kit/
├── global.json               # GERADO do pin — banda de SDK + runner de teste
├── Directory.Packages.props  # GERADO do pin — Central Package Management
├── Directory.Build.props     # TFM, Nullable, TreatWarningsAsErrors
├── NuGet.config              # <clear/> + nuget.org: restore não herda feed de máquina
├── Morph.AiKit.sln
├── src/Morph.AiKit/          # a biblioteca
└── tests/Morph.AiKit.Tests/  # xUnit v3, sem rede e sem chave de API
```

## Build e teste

```bash
cd framework/templates/dotnet/ai-kit
dotnet restore Morph.AiKit.sln
dotnet build   Morph.AiKit.sln -c Release --no-restore
dotnet test  --solution Morph.AiKit.sln -c Release --no-build
```

> **Nenhuma flag da era VSTest no `dotnet test` — `--nologo` mata a sessão.**
> O `global.json` coloca este kit no **modo MTP** do `dotnet test` (SDK .NET 10).
> Nesse modo o CLI não conhece `--nologo` e o repassa **literalmente** para o
> aplicativo de teste, que responde `error: unknown option: --nologo`: a sessão
> morre com o código de saída **5** e o relatório diz *"Zero testes executados"*
> — com os 115 testes passando por baixo. Medido em 2026‑09‑08. O `dotnet build
> --nologo` é inofensivo, e é isso que torna o erro fácil de cometer. Pela mesma
> razão o caminho da solução vai em `--solution`, e não como argumento
> posicional: é a grafia documentada do modo MTP (passo 5 do guia de migração).
> `--logger`, `--collect` e `--blame` caem na mesma armadilha.

> **O modo MTP muda também a SAÍDA, não só a invocação.** O sumário não é uma
> linha: é um **bloco de 6 linhas**, o veredito **não começa** a linha
> (`Resumo da execução de teste: Aprovado!` / `Test run summary: Passed!`) e a
> contagem vem em linha própria, indentada, com **`total:` minúsculo** — nada
> disso é o sumário do VSTest. Medido nos dois locales em 2026‑09‑08.
> Consequência: **um parser escrito só para o sumário do VSTest não conta os testes
> deste projeto** — um parser que procure uma linha *começando* em
> `Aprovado!`/`Passed!` **e** contendo `Total de testes:`/`Total:` falha nos dois
> eixos, e nos dois locales. O `morph-spec verify` lê o bloco MTP nos dois locales
> desde a issue #115; antes, a contagem deste projeto voltava desconhecida.

> **Descoberta parcial não pode passar verde.** O `.csproj` de teste declara
> `TestingPlatformCommandLineArguments` com o piso `--minimum-expected-tests N`
> (o número vive no `.csproj`): uma execução que rode **menos de N casos, mas
> pelo menos um,** sai com o código **9**; **zero** casos continua saindo **8**
> (`ZeroTests`) — o piso não supera esse veredito (medido duas vezes, com piso
> 275 e com piso 1). Sem piso, uma execução que descobre **1 de 115** imprime
> *"Aprovado!"* e sai **0** — medido. O piso mora no `.csproj`, e não no passo
> do workflow, para valer em toda invocação (CI, máquina do dev e o nó `tests`
> do `morph-spec verify`); por isso nenhum chamador repassa a flag. Ao adicionar
> testes, suba o piso — `test/framework/ai-kit-ci-packaging.test.js` reprova um
> piso que ficou para trás da contagem de `[Fact]`/`[Theory]`.
> **Consequência para o recorte por task:** `--minimum-expected-tests 1` na
> linha de comando não sobrescreve o valor do projeto (medido: o recorte verde
> sai 9 do mesmo jeito), mas a propriedade global do MSBuild sim. Por isso
> `morph-spec verify {f} {task}` estreita este projeto repassando
> `"-p:TestingPlatformCommandLineArguments=…"` com o valor do `.csproj` e o piso
> em 1 — verde sai 0, zero testes segue saindo 8, nada recompila (issue #114).
> Mantenha o piso **nesse** elemento, literal (sem `Condition`, sem `$(…)`, sem
> outra declaração da propriedade em `Directory.Build.*`): em qualquer outra
> forma o recorte volta a rodar o nó inteiro.

> **`dotnet test` precisa rodar COM O CWD DENTRO deste diretório.** O opt-in do
> Microsoft.Testing.Platform (que `xunit.v3` 4.x usa) vive em `global.json`, e o
> SDK procura o `global.json` a partir do diretório corrente subindo — não a
> partir do `.sln` passado no argumento. Invocado da raiz do repositório,
> `dotnet test framework/templates/dotnet/ai-kit/Morph.AiKit.sln` falha com
> *"Testing with VSTest target is no longer supported"*. O `dotnet build` **não**
> tem esse problema. No CI, use `working-directory:`.

## Versões de pacote

**Nenhum `.csproj` declara `Version`.** Central Package Management está ligado, e
todas as versões vivem em `Directory.Packages.props`, gerado por
`scripts/generate-ai-pin-props.js` a partir de `framework/ai-pin.json`. Para
mudar uma versão, mude o pin e rode `npm run generate`; `npm run generate:check`
(dentro de `npm run verify`) reprova qualquer divergência. Um `PackageReference`
sem `PackageVersion` correspondente falha o restore com **NU1010** — é isso que
torna a divergência estruturalmente impossível, em vez de apenas detectável.

`bin/` e `obj/` ficam fora do git e fora do tarball npm pelo `.gitignore` deste
diretório. Uma regra no `.npmignore` da raiz **não** funcionaria: `package.json`
→ `files` lista `framework/`, e o npm não deixa um ignore da raiz excluir nada
sob um diretório de `files`.

## Dois níveis de garantia: compilação × contrato de versão

O pin tem **oito** pacotes; o kit **não** os prova todos da mesma forma, e a
diferença é material — não é detalhe de empacotamento.

| Pacote do pin | Garantia | O que reprova a PR |
|---|---|---|
| `Microsoft.Agents.AI` | **cobertura de compilação** | um rename de API: o kit tem `using` reais e o build quebra |
| `Microsoft.Extensions.AI` | **cobertura de compilação** | idem |
| `Microsoft.Extensions.AI.Evaluation` | **cobertura de compilação** | `Evals/EvalAssertion.cs` implementa `IEvaluator` |
| `Microsoft.Extensions.AI.Evaluation.Quality` | **cobertura de compilação** | `Evals/EvalQualityEvaluators.cs` constrói os avaliadores estáveis |
| `Microsoft.Extensions.AI.Evaluation.Reporting` | **cobertura de compilação** | `Evals/EvalFixture.cs` monta a `ReportingConfiguration` |
| `OpenAI` | **contrato de versão** | só o restore: `NU1102` se a versão sumir do nuget.org |
| `Google.GenAI` | **contrato de versão** | idem |
| `ModelContextProtocol` | **contrato de versão** | idem |
| `OpenAI` | **cobertura de compilação** | um rename: `Media/Providers/OpenAIImageProvider.cs` tem `using OpenAI.Images;` e usa `ImageClient`, `ImageEditOptions`, `GeneratedImageQuality` |
| `Google.GenAI` | **cobertura de compilação** | um rename: os dois adaptadores de `Media/Providers/` têm `using Google.GenAI.Types;` e usam `GenerateVideosSource`, `GenerateVideosOperation`, `Blob` |
| `ModelContextProtocol` | **contrato de versão** | só o restore: `NU1102` se a versão sumir do nuget.org |

Sem eufemismo: para **um** deles, **um rename de API não reprova a PR**. O job
`ai-kit` prova apenas que a versão pinada continua existindo no nuget.org — não
que a API dela continua a mesma. Não existe um único `using ModelContextProtocol`
em `src/` nem em `tests/`.

**A linha mudou, e vale registrar por quê.** Até a chegada de
`src/Morph.AiKit/Media/`, três dos cinco pacotes eram contrato de versão: o pin
não inclui `Microsoft.Extensions.AI.OpenAI`, e sem essa ponte o kit não tocava
tipo nenhum de `OpenAI` nem de `Google.GenAI`. O subsistema de mídia fechou a
lacuna **sem** acrescentar pacote: o adaptador `IImageGenerator` sobre
`ImageClient` está escrito à mão em `Media/Providers/OpenAIImageProvider.cs`, e o
caminho de vídeo do Veo usa `Google.GenAI` direto — que é o único caminho com SDK
.NET oficial, e por isso não havia ponte a esperar. Quem apontou a mudança foi o
próprio canário: `test/framework/ai-kit-package-coverage.test.js` ficou vermelho
nomeando os dois pacotes como entradas obsoletas de `RESTORE_ONLY`.

O `PackageReference` de `ModelContextProtocol` **fica** no `.csproj` de propósito:
sem alguém referenciando o pacote, o restore nunca o resolve, e aí some até a
régua fraca (o `NU1102`) que existe hoje. Fechar essa última lacuna é expor uma
superfície MCP de verdade no kit — escopo novo, decisão do dono do pin.

Essa classificação não é prosa solta: `test/framework/ai-kit-package-coverage.test.js`
exige que **todo** `PackageReference` do `Morph.AiKit.csproj` esteja ou
exercitado por um `using` real em `src/`, ou listado como restore-only na
constante do próprio teste. Um pacote novo que não caia em nenhum dos dois casos
deixa o teste vermelho nomeando o pacote — a prosa acima não pode envelhecer em
silêncio.
