diff --git a/AGENTS.md b/AGENTS.md index ea11a99..6da2716 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 --topic + ``` + + Repetir `--topic ` 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 diff --git a/package.json b/package.json index 201902f..374badc 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/scripts/t3-publication-preflight.cjs b/scripts/t3-publication-preflight.cjs new file mode 100644 index 0000000..1d3522a --- /dev/null +++ b/scripts/t3-publication-preflight.cjs @@ -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 }; diff --git a/tests/content/t3-publication-preflight.spec.cjs b/tests/content/t3-publication-preflight.spec.cjs new file mode 100644 index 0000000..94d72c7 --- /dev/null +++ b/tests/content/t3-publication-preflight.spec.cjs @@ -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();