diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2c0ec64..c29f8c9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,6 +16,7 @@ jobs: - run: node scripts/verify-manifest-consistency.mjs - run: node scripts/verify-cargo-examples.mjs - run: node scripts/verify-chapter-anatomy.mjs + - run: node scripts/verify-review-packet-coverage.mjs - run: node scripts/verify-editorial-status.mjs - run: cargo fmt --check - run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/README.md b/README.md index 8bded7a..16b8c79 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ node scripts/verify-course-links.mjs node scripts/verify-manifest-consistency.mjs node scripts/verify-cargo-examples.mjs node scripts/verify-chapter-anatomy.mjs +node scripts/verify-review-packet-coverage.mjs node scripts/verify-editorial-status.mjs ``` diff --git a/docs/compuertas-automaticas.md b/docs/compuertas-automaticas.md index 10ce3b7..cb0cf1e 100644 --- a/docs/compuertas-automaticas.md +++ b/docs/compuertas-automaticas.md @@ -17,6 +17,7 @@ un estado editorial superior. | `node scripts/verify-manifest-consistency.mjs` | Que `course.manifest.json` mantenga identidad, orden, unicidad y rutas derivadas del número y slug de cada capítulo. | Número duplicado, slug inválido, documento mal nombrado o ruta inconsistente. | Joel decide si el manifiesto representa el contrato correcto para `academy-web`. | | `node scripts/verify-cargo-examples.mjs` | Que todos los ejemplos `.rs` de `examples/` estén declarados explícitamente en `Cargo.toml` y que cada ruta declarada exista. | Ejemplo sin declarar, ruta inexistente, nombre duplicado o ruta duplicada. | Joel decide si el ejemplo enseña el concepto con claridad suficiente. | | `node scripts/verify-chapter-anatomy.mjs` | Que cada capítulo conserve la anatomía editorial esperada antes de revisión humana. | Falta concepto, problema, alternativas, justificación, invariantes, modelo Rust, ejemplo, práctica o cierre editorial. | Joel decide si la explicación es correcta, suficiente y aprobable. | +| `node scripts/verify-review-packet-coverage.mjs` | Que el paquete de revisión humana cubra todos los capítulos y sus artefactos principales. | Falta un capítulo, ruta, comando de compuerta o frontera editorial. | Joel decide el resultado de la revisión; la compuerta solo evita omisiones. | | `node scripts/verify-editorial-status.mjs` | Que ningún capítulo se marque como `reviewed` ni `published` por automatización. | Estado editorial elevado sin revisión humana explícita. | Solo Joel puede aprobar revisión o publicación de capítulos. | | `cargo fmt --check` | Que el código Rust conserve formato estándar. | Archivo Rust fuera de formato. | No sustituye criterio sobre legibilidad pedagógica. | | `cargo clippy --all-targets --all-features -- -D warnings` | Que biblioteca, pruebas y ejemplos no acumulen advertencias relevantes. | Warning de Clippy o patrón de código riesgoso. | Joel decide si una excepción futura requiere explicación curricular. | diff --git a/docs/publicacion-candidata.md b/docs/publicacion-candidata.md index bf1e1cb..95d6d88 100644 --- a/docs/publicacion-candidata.md +++ b/docs/publicacion-candidata.md @@ -41,6 +41,7 @@ Antes de considerar este corte como listo para revisión humana, deben pasar: - `node scripts/verify-manifest-consistency.mjs`; - `node scripts/verify-cargo-examples.mjs`; - `node scripts/verify-chapter-anatomy.mjs`; +- `node scripts/verify-review-packet-coverage.mjs`; - `cargo fmt --check`; - `cargo clippy --all-targets --all-features -- -D warnings`; - `cargo test --all-targets`; diff --git a/scripts/verify-review-packet-coverage.mjs b/scripts/verify-review-packet-coverage.mjs new file mode 100644 index 0000000..f454fb0 --- /dev/null +++ b/scripts/verify-review-packet-coverage.mjs @@ -0,0 +1,100 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const root = join(import.meta.dirname, ".."); +const manifest = JSON.parse( + readFileSync(join(root, "course.manifest.json"), "utf8"), +); +const packetPath = join(root, "docs", "paquete-revision-humana.md"); +const packet = readFileSync(packetPath, "utf8"); +const failures = []; + +function fail(message) { + failures.push(message); +} + +const requiredGlobalSections = [ + "# Paquete de revisión humana", + "## Frontera de decisión", + "## Compuertas antes de revisar", + "## Preguntas por capítulo", + "## Capítulos", + "## Resultado de la revisión", +]; + +for (const section of requiredGlobalSections) { + if (!packet.includes(section)) { + fail(`falta sección global: ${section}`); + } +} + +for (const command of [ + "node scripts/verify-course-links.mjs", + "node scripts/verify-manifest-consistency.mjs", + "node scripts/verify-cargo-examples.mjs", + "node scripts/verify-chapter-anatomy.mjs", + "node scripts/verify-editorial-status.mjs", + "cargo fmt --check", + "cargo clippy --all-targets --all-features -- -D warnings", + "cargo test --all-targets", + "cargo test --doc", + "cargo bench --all-targets", + "git diff --check", +]) { + if (!packet.includes(command)) { + fail(`falta comando de compuerta: ${command}`); + } +} + +for (const chapter of manifest.chapters) { + const prefix = String(chapter.number).padStart(2, "0"); + const heading = `### ${prefix}. ${chapter.title}`; + const headingIndex = packet.indexOf(heading); + + if (headingIndex === -1) { + fail(`${chapter.title}: falta encabezado ${heading}`); + continue; + } + + const nextHeadingIndex = packet.indexOf("\n### ", headingIndex + heading.length); + const chapterSection = packet.slice( + headingIndex, + nextHeadingIndex === -1 ? packet.length : nextHeadingIndex, + ); + + for (const [field, label] of [ + ["document", "Documento"], + ["exercises", "Ejercicios"], + ["costs", "Costos"], + ["module", "Módulo Rust"], + ["example", "Ejemplo"], + ["tests", "Tests"], + ["diagram", "Diagrama"], + ]) { + const expectedLine = `- ${label}: \`${chapter[field]}\``; + + if (!chapterSection.includes(expectedLine)) { + fail(`${chapter.title}: falta ${expectedLine}`); + } + } + + if (!chapterSection.includes("- Revisión humana:")) { + fail(`${chapter.title}: falta criterio de revisión humana`); + } +} + +if (!packet.includes("`reviewed`") || !packet.includes("`published`")) { + fail("el paquete debe conservar explícita la frontera reviewed/published"); +} + +if (failures.length > 0) { + console.error("Paquete de revisión humana incompleto:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log( + `Paquete de revisión humana verificado: ${manifest.chapters.length} capítulos cubiertos.`, +);