Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ on:
- develop

jobs:
validate-seed:
name: Validate curated seed on PostgreSQL
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: seed_test
POSTGRES_PASSWORD: seed_test_only
POSTGRES_DB: sinapse_seed_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U seed_test -d sinapse_seed_test"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
NODE_ENV: test
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: seed_test
POSTGRES_PASSWORD: seed_test_only
POSTGRES_DB: sinapse_seed_test
SEED_TEST_DATABASE_URL: postgresql://seed_test:seed_test_only@localhost:5432/sinapse_seed_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: backend/package-lock.json
- run: npm ci
working-directory: backend
- run: npm run migrate
working-directory: backend
- run: npm run seed:validate
working-directory: backend
- run: npm run test:seed
working-directory: backend

validate-environment:
name: Validate Workflows and Docker Setup
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"private": true,
"main": "dist/index.js",
"scripts": {
"seed:validate": "tsx src/database/seed.ts --validate",
"seed:apply": "tsx src/database/seed.ts --apply",
"test:seed": "tsx --test src/database/seed.test.ts",
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
Expand Down
83 changes: 83 additions & 0 deletions backend/src/database/seed-lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { isDeepStrictEqual } from "node:util";
import type { PoolClient } from "pg";

const fields: Record<string, string[]> = {
projeto: ["id", "nome", "cliente", "descricao", "status"],
documento: ["id", "projeto_id", "nome", "mime", "caminho", "status_processamento"],
chunk: ["id", "projeto_id", "entidade_tipo", "entidade_id", "texto", "metadados_json"],
};
export interface SeedRecord { table: string; values: Record<string, unknown>; source: Record<string, string> }
export interface Dataset { dataset: string; version: number; source_decision: Record<string, string>; records: SeedRecord[] }
export const fixturePath = resolve(process.cwd(), "../database/seed/fixtures/historical-v1.json");

export function validateDataset(input: unknown): asserts input is Dataset {
const data = input as Dataset;
if (data?.dataset !== "pre06-historical-v1" || data.version !== 1 || !data.source_decision?.decision || !Array.isArray(data.records) || !data.records.length) throw new Error("Manifesto inválido");
const seen = new Map<string, SeedRecord>();
for (const row of data.records) {
const columns = fields[row.table];
if (!columns || !row.values || !isDeepStrictEqual(Object.keys(row.values).sort(), [...columns].sort())) throw new Error("Tabela ou campos não permitidos");
const id = row.values.id;
if (typeof id !== "string" || !/^60000000-0000-4000-8000-\d{12}$/.test(id) || seen.has(id)) throw new Error("ID inválido ou duplicado");
const source = row.source;
if (!source || !/^https:\/\/github.com\/Galaticos-API\/API-[123]$/.test(source.repository)
|| !/^[a-f0-9]{40}$/.test(source.revision) || !/^[a-f0-9]{64}$/.test(source.source_sha256)
|| !/^(readme\.md|DOCS\/Documentação das Sprints\/DocSprint1\.md|DOCS\/analise_backend\/analiseRequisitosBackend\.md)$/.test(source.path)
|| !source.locator || source.classification !== "curated-public-documentation"
|| !source.transformation || !source.url?.startsWith(`${source.repository}/blob/${source.revision}/`)) throw new Error("Origem ausente ou não permitida");
if (row.table === "documento") {
if (seen.get(String(row.values.projeto_id))?.table !== "projeto") throw new Error("Projeto de documento inválido");
if (!/^database\/seed\/curated\/[a-z0-9-]+\.md$/.test(String(row.values.caminho))) throw new Error("Caminho não permitido");
}
if (row.table === "chunk") {
const parent = seen.get(String(row.values.entidade_id));
const metadata = row.values.metadados_json as Record<string, unknown>;
if (row.values.entidade_tipo !== "documento" || parent?.table !== "documento" || parent.values.projeto_id !== row.values.projeto_id
|| metadata?.dataset !== data.dataset || metadata.source_url !== source.url || metadata.embedding_status !== "pending") throw new Error("Chunk sem isolamento ou origem");
}
seen.set(id, row);
}
}

export async function loadDataset(): Promise<Dataset> {
const data: unknown = JSON.parse(await readFile(fixturePath, "utf8"));
validateDataset(data);
for (const row of data.records.filter(row => row.table === "documento")) {
const text = await readFile(resolve(process.cwd(), "..", String(row.values.caminho)), "utf8");
if (!text.includes(row.source.url)) throw new Error("Documento curado sem referência");
}
return data;
}

export function validateTarget(url: string | undefined, environment: string | undefined): string {
if (!url || environment === "production") throw new Error("Seed restrito a banco de desenvolvimento/teste explícito");
const target = new URL(url);
if (!["postgres:", "postgresql:"].includes(target.protocol) || !/_(dev|test)$/.test(decodeURIComponent(target.pathname))) throw new Error("Banco deve terminar em _dev ou _test");
return url;
}

// Caller owns BEGIN/COMMIT/ROLLBACK. No partial application, update or deletion.
export async function applyDataset(client: PoolClient, data: Dataset): Promise<void> {
validateDataset(data);
await client.query("SELECT pg_advisory_xact_lock(604006)");
for (const row of data.records) {
const keys = fields[row.table];
const id = String(row.values.id);
const auditId = id.replace(/^60000000/, "61000000");
const evidence = { dataset: data.dataset, version: data.version, source: row.source,
payload_sha256: createHash("sha256").update(JSON.stringify(row.values)).digest("hex") };
const existing = await client.query(`SELECT ${keys.join(",")} FROM ${row.table} WHERE id = $1`, [id]);
const audit = await client.query("SELECT dados_json FROM auditoria WHERE id = $1", [auditId]);
if (existing.rowCount) {
if (!isDeepStrictEqual(existing.rows[0], row.values) || !isDeepStrictEqual(audit.rows[0]?.dados_json, evidence)) throw new Error("Colisão ou divergência em registro do seed; nenhuma sobrescrita realizada");
continue;
}
if (audit.rowCount) throw new Error("Origem existente sem registro correspondente");
await client.query(`INSERT INTO ${row.table} (${keys.join(",")}) VALUES (${keys.map((_, i) => `$${i + 1}`).join(",")})`, keys.map(key => row.values[key]));
await client.query("INSERT INTO auditoria (id, entidade_tipo, entidade_id, acao, justificativa, dados_json) VALUES ($1,$2,$3,'seed_import',$4,$5)",
[auditId, row.table, id, "PRE-06: carga curada de documentação pública; origem registrada", evidence]);
}
}
50 changes: 50 additions & 0 deletions backend/src/database/seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import { applyDataset, loadDataset, validateDataset, validateTarget } from "./seed-lib.js";
import { Pool } from "pg";

test("acervo curado contém três projetos, seis documentos e seis chunks com origem", async () => {
const data = await loadDataset();
assert.equal(data.records.filter(row => row.table === "projeto").length, 3);
assert.equal(data.records.filter(row => row.table === "documento").length, 6);
assert.equal(data.records.filter(row => row.table === "chunk").length, 6);
assert.equal(data.records.some(row => ["usuario", "competencia", "alocacao"].includes(row.table)), false);
});
test("recusa registro sem origem", async () => {
const data = await loadDataset(); data.records[0].source.revision = "";
assert.throws(() => validateDataset(data));
});
test("recusa mistura de projetos nos chunks", async () => {
const data = await loadDataset(); data.records.find(row => row.table === "chunk")!.values.projeto_id = "outro";
assert.throws(() => validateDataset(data));
});
test("recusa tabelas e caminhos fora da curadoria", async () => {
const data = await loadDataset(); data.records[0].table = "usuario";
assert.throws(() => validateDataset(data));
const other = await loadDataset(); other.records.find(row => row.table === "documento")!.values.caminho = "../../secret";
assert.throws(() => validateDataset(other));
});
test("recusa produção, destino ausente e banco não dedicado", () => {
assert.throws(() => validateTarget(undefined, "test"));
assert.throws(() => validateTarget("postgresql://localhost/acervo_test", "production"));
assert.throws(() => validateTarget("postgresql://localhost/sinapse", "development"));
assert.equal(validateTarget("postgresql://localhost/acervo_test", "test"), "postgresql://localhost/acervo_test");
});
test("carga SQL é idempotente e recusa sobrescrita", { skip: !process.env.SEED_TEST_DATABASE_URL }, async () => {
const pool = new Pool({ connectionString: validateTarget(process.env.SEED_TEST_DATABASE_URL, "test") });
const client = await pool.connect();
try {
await client.query("BEGIN");
const data = await loadDataset();
await applyDataset(client, data);
await applyDataset(client, data);
const count = await client.query("SELECT count(*)::int AS total FROM auditoria WHERE dados_json->>'dataset' = $1", [data.dataset]);
assert.equal(count.rows[0].total, 15);
await client.query("SAVEPOINT collision");
const changed = structuredClone(data); changed.records[0].values.nome = "Alterado";
await assert.rejects(applyDataset(client, changed));
await client.query("ROLLBACK TO SAVEPOINT collision");
const stored = await client.query("SELECT nome FROM projeto WHERE id = $1", [data.records[0].values.id]);
assert.equal(stored.rows[0].nome, data.records[0].values.nome);
} finally { await client.query("ROLLBACK"); client.release(); await pool.end(); }
});
30 changes: 30 additions & 0 deletions backend/src/database/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Pool } from "pg";
import { applyDataset, loadDataset, validateTarget } from "./seed-lib.js";

async function main() {
const mode = process.argv[2] ?? "--validate";
if (!["--validate", "--apply"].includes(mode)) throw new Error("Modo inválido");
const data = await loadDataset();
if (mode === "--validate") {
console.log(`[Seed] Manifesto e documentos válidos: ${data.records.length} registros.`);
return;
}
const connectionString = validateTarget(process.env.SEED_DATABASE_URL, process.env.NODE_ENV);
const pool = new Pool({ connectionString, connectionTimeoutMillis: 5000 });
try {
const client = await pool.connect();
try {
await client.query("BEGIN");
await applyDataset(client, data);
await client.query("COMMIT");
console.log(`[Seed] Carga concluída: ${data.records.length} registros com origem.`);
} catch (error) { await client.query("ROLLBACK"); throw error; }
finally { client.release(); }
} finally { await pool.end(); }
}

main().catch(() => {
// Never dump connection strings, database details or source payloads to logs.
console.error("[Seed] Falha na validação/carga. Confira migrações, banco permitido e colisões; transação não confirmada.");
process.exitCode = 1;
});
60 changes: 60 additions & 0 deletions database/seed/POLITICA_DE_DADOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Política de dados iniciais — PRE-06

## Decisão e finalidade

Em 14/09/2026, o solicitante confirmou nesta tarefa o uso de API-1, API-2 e API-3,
seguindo a origem D3 registrada em `docs/backlog/README.md`. Não foi adotado Plano B
fictício. Finalidade: desenvolvimento e validação local de busca e reúso documental.
A base é um recorte de requisitos públicos, não uma cópia dos sistemas anteriores.

## Curadoria aplicada

- API-1: `readme.md`, itens 23 e 35 do backlog (acesso administrativo e gráficos).
- API-2: `DOCS/Documentação das Sprints/DocSprint1.md`, US-01 e US-02.
- API-3: `DOCS/analise_backend/analiseRequisitosBackend.md`, GRF-01 e GRF-08.

Entram apenas resumos dos requisitos selecionados. Ficam fora autores, nomes de
colaboradores, contatos, clientes identificáveis, código-fonte, configuração de IDE,
segredos, anexos de teste, imagens de atestados, avaliações individuais e documentos
brutos. Os clientes aparecem como “Organização não identificada”; não existe tabela
de correspondência com pessoas no seed. Referências aos repositórios são mantidas
para rastreabilidade, portanto não se promete anonimização absoluta das fontes públicas.

Os textos são resumos produzidos com assistência de IA, sujeitos à revisão do PR.
Não foram convertidos em histórias ou critérios adicionais que não existam nas fontes.
Links, revisões e hashes identificam a evidência consultada, sem importar o documento
inteiro nem os dados de exemplos operacionais. O hash registra o texto fonte obtido
em UTF-8; não é uma alegação de aprovação jurídica ou de conteúdo íntegro de um PDF.

## Origem e integridade

Cada entidade possui origem no manifesto e um evento determinístico `seed_import`
na tabela `auditoria`. Chunks também carregam URL, revisão e hash nos metadados.
Eventos não recebem autor humano fictício; `usuario_id` permanece nulo e a seleção
das fontes fica documentada no manifesto. O carregador usa parâmetros SQL e emite
somente contagens ou erro genérico, sem URLs de conexão ou payloads em logs.

## Uso e limitações

A escolha das fontes foi autorizada pelo solicitante. A revisão humana do recorte
deve ocorrer no PR antes de sua distribuição. O carregador é restrito a ambientes
de desenvolvimento/teste; não usar em produção ou em base compartilhada de cliente.

Não se infere competência individual a partir de tecnologias do repositório. Não
foram carregados usuários, alocações ou competências pessoais; consultas “quem
trabalhou com X?” devem retornar ausência de evidência individual. Para demonstrar
respostas positivas do PBI-05.2.1, o time ainda precisa fornecer evidência de alocação
curada e autorizada, sem inventar vínculos entre pessoas e requisitos.

Busca semântica exige embeddings reais. Os chunks têm vetor nulo e metadado
`embedding_status=pending`; documentos continuam `pendente` para indexação. Os casos
de validação descrevem expectativas do acervo, não resultados de um motor já testado.

## Revisão, retenção e retirada

Toda ampliação exige origem versionada, curadoria de dados pessoais e revisão por
outra pessoa. Não importar diretórios inteiros. Se uma fonte precisar ser retirada,
identificar seus IDs pelo manifesto, revisar referências e excluir conteúdo e
vetores em uma migração aprovada; preservar apenas a trilha de retirada necessária,
sem reter conteúdo sensível no evento. Ambientes descartáveis devem ser eliminados
ao final dos testes. Nunca apagar ou alterar silenciosamente dados fora do dataset.
42 changes: 42 additions & 0 deletions database/seed/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,50 @@
# Seed de desenvolvimento

## PRE-06 — Acervo histórico curado

O seed atual é `fixtures/historical-v1.json`, aplicado pelo backend. Contém três
projetos acadêmicos API-1/API-2/API-3, seis resumos de requisitos e seis chunks sem
vetores. As cópias curadas ficam em `curated/`. Não há pessoas, credenciais ou
competências inventadas. Cada registro tem repositório, revisão, caminho, localização,
hash SHA-256 do texto fonte em UTF-8 e descrição da transformação; a carga persiste
essa origem em `auditoria` e nos metadados dos chunks.

Dentro de `backend`, execute `npm ci`, `npm run seed:validate` e `npm run test:seed`.
Para carregar, prepare um banco dedicado com as migrações 001–003. Configure
`SEED_DATABASE_URL` por variável de ambiente (não em argumentos nem arquivos versionados)
e execute `npm run seed:apply`. O nome do banco deve terminar em `_dev` ou `_test`;
`NODE_ENV=production` é recusado. O comando de migração existente usa suas próprias
variáveis `POSTGRES_*`; confira que apontam para o mesmo banco dedicado antes de migrar.

O modo padrão só valida arquivos. A aplicação é transacional, serializada e
idempotente: repetir não duplica registros/auditoria; colisões ou alteração de
conteúdo/origem interrompem a operação, sem sobrescrever dados do usuário. Não há
remoção automática. Para atualizar o acervo, criar versão revisada e migração explícita.

`test:seed` verifica o manifesto sem PostgreSQL. Se `SEED_TEST_DATABASE_URL` estiver
definida, também testa a carga duas vezes em uma transação revertida ao final e a
recusa de divergência. A CI fornece PostgreSQL 16 + pgvector dedicado e executa as
migrações antes desse teste. Sem a variável, o teste SQL aparece como ignorado;
isso não equivale a validação da carga real.

Leia [POLITICA_DE_DADOS.md](POLITICA_DE_DADOS.md) e [validation-cases.json](validation-cases.json).
Embeddings não são fabricados: a indexação real continua pendente do pipeline da IA.

Validação local: manifesto e documentos aprovados pelo validador, build do backend
concluído e 6 testes da PRE-06 passaram com PostgreSQL 16 + pgvector descartável,
sem testes ignorados. O comando `seed:apply` também foi executado duas vezes com
sucesso: 3 projetos, 6 documentos, 6 chunks e 15 eventos de origem, sem duplicação.
O banco de teste foi removido; nenhuma carga foi feita na base corrente do projeto.
A execução remota da CI e a revisão humana do PR continuam pendentes.

## Seed fictício legado

`dev_seed.sql` contém somente dados fictícios e determinísticos para desenvolvimento
local e demonstrações. Não use este arquivo em produção e não inclua dados reais,
tokens, senhas ou documentos de clientes.

O seed é idempotente: pode ser executado novamente sem duplicar os registros com os
mesmos identificadores.

O arquivo legado é mantido por compatibilidade, não faz parte da carga histórica
PRE-06 e não deve ser executado junto com ela para demonstrar o acervo curado.
7 changes: 7 additions & 0 deletions database/seed/curated/api-1-backlog-23.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Acesso administrativo

O backlog prevê login para administradores, restringindo o acesso às funcionalidades destinadas aos responsáveis.

Origem: https://github.com/Galaticos-API/API-1/blob/1edd3096e46cb1cb9c794dbaec887a83487c0e97/readme.md#L68

Classificação: resumo curado de documentação pública. Não comprova implementação em produção nem experiência individual.
7 changes: 7 additions & 0 deletions database/seed/curated/api-1-backlog-35.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Métricas de equipe em gráficos

O backlog prevê gráficos de avaliação para apoiar a análise do desempenho de equipes.

Origem: https://github.com/Galaticos-API/API-1/blob/1edd3096e46cb1cb9c794dbaec887a83487c0e97/readme.md#L74

Classificação: resumo curado de documentação pública. Não comprova implementação em produção nem experiência individual.
7 changes: 7 additions & 0 deletions database/seed/curated/api-2-us-01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Administrar acesso de usuários

A documentação da primeira sprint descreve cadastro, consulta, edição e inativação de usuários pelo RH para administrar o acesso à plataforma.

Origem: https://github.com/Galaticos-API/API-2/blob/e568684bc66a8a339a04a1f091d9599754d21cf6/DOCS/Documenta%C3%A7%C3%A3o%20das%20Sprints/DocSprint1.md#L43

Classificação: resumo curado de documentação pública. Não comprova implementação em produção nem experiência individual.
7 changes: 7 additions & 0 deletions database/seed/curated/api-2-us-02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Registrar plano individual por ano

A documentação descreve criação de um plano de desenvolvimento individual associado a um colaborador e a um ano, preservando o histórico de planos.

Origem: https://github.com/Galaticos-API/API-2/blob/e568684bc66a8a339a04a1f091d9599754d21cf6/DOCS/Documenta%C3%A7%C3%A3o%20das%20Sprints/DocSprint1.md#L44

Classificação: resumo curado de documentação pública. Não comprova implementação em produção nem experiência individual.
Loading
Loading