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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@

- Conteudo T3 deve respeitar manifesto curricular, cobertura e revisoes
pedagogica e linguistica, alem da aprovacao humana aplicavel.
- Antes de criar ou ampliar conteudo T3, executar o preflight apenas para os
temas autorizados:

```sh
npm run preflight:t3 -- --subject <materia> --topic <tema>
```

Repetir `--topic <tema>` para cada tema. Codigo 2 significa que pelo menos
um tema solicitado ja esta publicado e completo: interromper antes de
escrever e revisar o escopo; codigo 1 indica entrada ou estado invalido;
codigo 0 indica que o trabalho solicitado nao esta completo, sem substituir
revisao editorial ou aprovacao humana.

- Automacao, parecer de agente ou passagem nos checks nao autorizam merge,
release ou deploy por si so.
- Mudancas para `main` exigem PR, checks obrigatorios e autorizacao explicita
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"validate:mcp-governance": "node scripts/governance-mcp-server.cjs --validate",
"test:mcp-governance": "node tests/agents/governance-mcp-server.spec.cjs",
"validate:content": "node scripts/validate-content.cjs",
"preflight:t3": "node scripts/t3-publication-preflight.cjs",
"create:t3-pilot-draft": "node scripts/create-t3-pilot-draft.cjs",
"create:t3-geografia-agua-recurso": "node scripts/create-t3-geografia-agua-recurso.cjs",
"create:t3-geografia-nascente-rio": "node scripts/create-t3-geografia-nascente-rio.cjs",
Expand Down Expand Up @@ -57,7 +58,7 @@
"build:t3-portugues-expansion": "node scripts/build-t3-portugues-expansion.cjs",
"test:branch-policy": "node tests/governance/branch-policy.spec.cjs && node tests/governance/branch-sync.spec.cjs && node tests/governance/merge-message-policy.spec.cjs",
"test:guidelines": "node tests/governance/governance-guidelines.spec.cjs",
"test:content": "node tests/content/content-validator.spec.cjs && node tests/content/content-coverage-validator.spec.cjs && node tests/content/t3-pilot.spec.cjs && node tests/content/t3-geografia-agua-recurso.spec.cjs && node tests/content/t3-geografia-nascente-rio.spec.cjs && node tests/content/t3-geografia-agua-limitada.spec.cjs && node tests/content/t3-geografia-agua-brasil.spec.cjs && node tests/content/t3-geografia-usos-agua.spec.cjs && node tests/content/t3-ciencias-corpos-luz-release.spec.cjs && node tests/content/t3-portugues-release.spec.cjs && node tests/content/t3-matematica-release.spec.cjs && node tests/content/t3-ciencias-materiais-luz.spec.cjs && node tests/content/t3-ciencias-propriedades-luz.spec.cjs && node tests/content/t3-ciencias-espelhos.spec.cjs && node tests/content/t3-ciencias-luz-cores.spec.cjs && node tests/content/t3-ciencias-som-audicao.spec.cjs && node tests/content/t3-ingles-present-continuous.spec.cjs && node tests/content/t3-ingles-places-city.spec.cjs && node tests/content/t3-ingles-celebrations.spec.cjs && node tests/content/t3-ingles-means-transport.spec.cjs && node tests/content/t3-historia-povo-brasileiro.spec.cjs && node tests/content/t3-historia-imigrantes-ibericos.spec.cjs && node tests/content/t3-historia-imigrantes-italianos.spec.cjs && node tests/content/t3-historia-imigrantes-alemaes.spec.cjs && node tests/content/t3-historia-imigrantes-japoneses.spec.cjs && node tests/content/t3-portugues-usos-c.spec.cjs && node tests/content/t3-portugues-verbos.spec.cjs && node tests/content/t3-matematica-editorial.spec.cjs",
"test:content": "node tests/content/content-validator.spec.cjs && node tests/content/content-coverage-validator.spec.cjs && node tests/content/t3-pilot.spec.cjs && node tests/content/t3-geografia-agua-recurso.spec.cjs && node tests/content/t3-geografia-nascente-rio.spec.cjs && node tests/content/t3-geografia-agua-limitada.spec.cjs && node tests/content/t3-geografia-agua-brasil.spec.cjs && node tests/content/t3-geografia-usos-agua.spec.cjs && node tests/content/t3-ciencias-corpos-luz-release.spec.cjs && node tests/content/t3-portugues-release.spec.cjs && node tests/content/t3-matematica-release.spec.cjs && node tests/content/t3-ciencias-materiais-luz.spec.cjs && node tests/content/t3-ciencias-propriedades-luz.spec.cjs && node tests/content/t3-ciencias-espelhos.spec.cjs && node tests/content/t3-ciencias-luz-cores.spec.cjs && node tests/content/t3-ciencias-som-audicao.spec.cjs && node tests/content/t3-ingles-present-continuous.spec.cjs && node tests/content/t3-ingles-places-city.spec.cjs && node tests/content/t3-ingles-celebrations.spec.cjs && node tests/content/t3-ingles-means-transport.spec.cjs && node tests/content/t3-historia-povo-brasileiro.spec.cjs && node tests/content/t3-historia-imigrantes-ibericos.spec.cjs && node tests/content/t3-historia-imigrantes-italianos.spec.cjs && node tests/content/t3-historia-imigrantes-alemaes.spec.cjs && node tests/content/t3-historia-imigrantes-japoneses.spec.cjs && node tests/content/t3-portugues-usos-c.spec.cjs && node tests/content/t3-portugues-verbos.spec.cjs && node tests/content/t3-matematica-editorial.spec.cjs && node tests/content/t3-publication-preflight.spec.cjs",
"test:frontend-security": "node tests/security/frontend-rendering.spec.cjs && node tests/ui/safe-rendering.spec.cjs",
"test:pedagogical-regression": "node scripts/validate-pedagogical-regressions.cjs --scan-t2 && node tests/content/pedagogical-regression.spec.cjs",
"test:ui:content-session": "node tests/ui/content-session.spec.cjs",
Expand Down
116 changes: 116 additions & 0 deletions scripts/t3-publication-preflight.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const path = require('node:path');
const {
loadContentCatalog,
loadContentSources,
loadCoverageManifest,
} = require('./validate-content.cjs');

const ROOT = path.resolve(__dirname, '..');
const CONTENT_SET_ID = '2026-t3-v1';

function parseArguments(args) {
const result = { subject: null, topics: [] };
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
if (argument !== '--subject' && argument !== '--topic') {
throw new Error('argumento invalido: ' + argument);
}
const value = args[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(argument + ' exige um valor');
}
if (argument === '--subject') {
if (result.subject) throw new Error('--subject duplicado');
result.subject = value;
} else {
result.topics.push(value);
}
index += 1;
}
if (!result.subject) throw new Error('--subject obrigatorio');
if (result.topics.length === 0)
throw new Error('informe ao menos um --topic');
if (new Set(result.topics).size !== result.topics.length) {
throw new Error('--topic duplicado');
}
return result;
}

function evaluateTopics({
sources,
coverageManifest,
contentCatalog,
subject,
topics,
}) {
const coverage = coverageManifest?.contentSets?.[CONTENT_SET_ID]?.subjects;
const expectedTopics = coverage?.[subject];
if (!expectedTopics) {
throw new Error('materia nao declarada no manifesto T3: ' + subject);
}

const contentSet = contentCatalog.find(
(entry) => entry.contentSetId === CONTENT_SET_ID,
);
if (!contentSet) throw new Error('acervo T3 ausente no catalogo');

const questions = sources[subject]?.questions || [];
const results = topics.map((topic) => {
const expected = expectedTopics[topic];
if (!Number.isInteger(expected) || expected < 1) {
throw new Error('tema nao declarado no manifesto T3: ' + topic);
}
const matching = questions.filter(
(question) =>
question.contentSetId === CONTENT_SET_ID && question.topic === topic,
);
const actual = matching.length;
if (actual > expected) {
throw new Error(
'cobertura acima da meta para ' + topic + ': atual=' + actual,
);
}
const complete =
contentSet.status === 'published' &&
actual === expected &&
matching.every((question) => question.reviewStatus === 'published');
return {
topic,
actual,
expected,
contentStatus: contentSet.status,
complete,
};
});

return {
schemaVersion: 't3-publication-preflight-v1',
contentSetId: CONTENT_SET_ID,
subject,
topics: results,
decision: results.some((result) => result.complete)
? 'stop_already_complete'
: 'proceed_to_planning',
};
}

function run(args = process.argv.slice(2), rootDirectory = ROOT) {
try {
const input = parseArguments(args);
const result = evaluateTopics({
sources: loadContentSources(rootDirectory),
coverageManifest: loadCoverageManifest(rootDirectory),
contentCatalog: loadContentCatalog(rootDirectory),
...input,
});
process.stdout.write(JSON.stringify(result) + '\n');
if (result.decision === 'stop_already_complete') process.exitCode = 2;
} catch (error) {
process.stderr.write(error.message + '\n');
process.exitCode = 1;
}
}

if (require.main === module) run();

module.exports = { evaluateTopics, parseArguments, run };
125 changes: 125 additions & 0 deletions tests/content/t3-publication-preflight.spec.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const assert = require('node:assert/strict');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const {
evaluateTopics,
parseArguments,
} = require('../../scripts/t3-publication-preflight.cjs');

const contentSetId = '2026-t3-v1';

function fixtures({ completed = ['tema-pronto'], incomplete = [] } = {}) {
const questions = [];
completed.forEach((topic) => {
for (let index = 0; index < 20; index += 1) {
questions.push({
contentSetId,
topic,
reviewStatus: 'published',
});
}
});
incomplete.forEach((topic) => {
for (let index = 0; index < 8; index += 1) {
questions.push({
contentSetId,
topic,
reviewStatus: 'draft',
});
}
});
const topics = Object.fromEntries(
[...completed, ...incomplete].map((topic) => [topic, 20]),
);
return {
sources: { portugues: { questions } },
coverageManifest: {
contentSets: { [contentSetId]: { subjects: { portugues: topics } } },
},
contentCatalog: [{ contentSetId, status: 'published' }],
};
}

function run() {
assert.deepEqual(
parseArguments([
'--subject',
'portugues',
'--topic',
'um',
'--topic',
'dois',
]),
{ subject: 'portugues', topics: ['um', 'dois'] },
);
assert.throws(() => parseArguments([]), /--subject obrigatorio/);
assert.throws(
() => parseArguments(['--subject', 'portugues']),
/informe ao menos um --topic/,
);
assert.throws(
() =>
parseArguments([
'--subject',
'portugues',
'--topic',
'um',
'--topic',
'um',
]),
/--topic duplicado/,
);

const completed = evaluateTopics({
...fixtures({ completed: ['tema-pronto'] }),
subject: 'portugues',
topics: ['tema-pronto'],
});
assert.equal(completed.decision, 'stop_already_complete');
assert.equal(completed.topics[0].actual, 20);

const incomplete = evaluateTopics({
...fixtures({ incomplete: ['tema-aberto'] }),
subject: 'portugues',
topics: ['tema-aberto'],
});
assert.equal(incomplete.decision, 'proceed_to_planning');
assert.equal(incomplete.topics[0].actual, 8);

const mixed = evaluateTopics({
...fixtures({ completed: ['tema-pronto'], incomplete: ['tema-aberto'] }),
subject: 'portugues',
topics: ['tema-pronto', 'tema-aberto'],
});
assert.equal(mixed.decision, 'stop_already_complete');

assert.throws(
() =>
evaluateTopics({
...fixtures(),
subject: 'portugues',
topics: ['tema-fora-do-manifesto'],
}),
/tema nao declarado no manifesto T3/,
);

const root = path.resolve(__dirname, '../..');
const actual = spawnSync(
process.execPath,
[
path.join(root, 'scripts/t3-publication-preflight.cjs'),
'--subject',
'portugues',
'--topic',
'adjetivos',
],
{ cwd: root, encoding: 'utf8' },
);
assert.equal(actual.status, 2);
assert.match(actual.stdout, /"decision":"stop_already_complete"/);
assert.match(actual.stdout, /"actual":20,"expected":20/);

console.log('t3-publication-preflight: ok');
}

run();
Loading