// Load FLDOE State Report of Schools (SRS) assessment files → per-grade / // per-subject proficiency (percent level 3 or above) with the number of // students as the denominator. One file per subject × grade × administration; // spring files only (pipeline_miami/discover_srs.py registers them). // Run: npx tsx scripts/loaders/miami/srs_assessments.ts import fs from "node:fs"; import { requireSource, startDataLoad, finishDataLoad, padSchoolNumber, parseCell, schoolUniverse, upsertFacts, sheetRows, runMain, SOURCES_ROOT, type Fact } from "./_lib"; const SCRIPT_PATH = "scripts/loaders/miami/srs_assessments.ts"; const norm = (s: unknown) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); /** source_id → { metricKey, year }. Handles both the discovered naming and the three first-fetch ids. */ function classify(sourceId: string): { metricKey: string; year: string } | null { let m = /^fldoe_srs_(fast_ela|fast_math|science)_(g\d+)_spring(\d{4})$/.exec(sourceId); if (m) return { metricKey: `${m[1]}_${m[2]}_proficiency`, year: `${Number(m[3]) - 1}-${m[3].slice(2)}` }; m = /^fldoe_srs_eoc_(\w+)_spring(\d{4})$/.exec(sourceId); if (m) return { metricKey: `eoc_${m[1]}_proficiency`, year: `${Number(m[2]) - 1}-${m[2].slice(2)}` }; const legacy: Record = { "fldoe_fast_ela_g3_srs_spring2025": { metricKey: "fast_ela_g3_proficiency", year: "2024-25" }, "fldoe_best_alg1_srs_spring2025": { metricKey: "eoc_alg1_proficiency", year: "2024-25" }, "fldoe_science_g5_srs_spring2023": { metricKey: "science_g5_proficiency", year: "2022-23" }, }; return legacy[sourceId] ?? null; } async function main() { const dirs = fs.readdirSync(SOURCES_ROOT).filter((d) => classify(d) !== null).sort(); const universe = await schoolUniverse(); for (const sourceId of dirs) { const { metricKey, year } = classify(sourceId)!; let src; try { src = requireSource(sourceId); } catch { console.warn(`[srs] ${sourceId}: not fetched; skipped`); continue; } const { rows } = sheetRows(src.filePath); // The header row is the first row containing a "District Number" cell. // 2024+ files put it in column 0; the 2023 science files put "Grade" // in column 0 and "District Number" in column 1, so locate the column // rather than assuming it. const h = rows.findIndex((r) => Array.isArray(r) && r.some((c) => norm(c) === "district number")); if (h < 0) { console.warn(`[srs] ${sourceId}: header not found; skipped`); continue; } const header = rows[h].map(norm); const iDist = header.findIndex((c) => c === "district number"); const iSch = header.findIndex((c) => c === "school number"); const iN = header.findIndex((c) => c.startsWith("number of students")); const iP = header.findIndex((c) => c.startsWith("percentage in level 3") || c.startsWith("percent in level 3") || c.includes("level 3 or above")); if (iSch < 0 || iP < 0) { console.warn(`[srs] ${sourceId}: columns not found in ${JSON.stringify(header)}; skipped`); continue; } const facts: Fact[] = []; let dade = 0; for (const r of rows.slice(h + 1)) { if (String(r[iDist] ?? "").trim().replace(/\.0$/, "") !== "13") continue; dade++; const num = padSchoolNumber(r[iSch]); if (!num || !universe.has(num)) continue; const p = parseCell(r[iP]), n = iN >= 0 ? parseCell(r[iN]) : { value: null }; if (p.value === null && !p.suppressed) continue; facts.push({ schoolNumber: num, year, metricKey, value: p.value, suppressed: p.suppressed, denominator: n.value }); } const load = await startDataLoad({ sourceId, sourceUrl: src.prov.url, sha256: src.prov.sha256, bytes: src.prov.bytes, yearCovered: year, scriptPath: SCRIPT_PATH }); const res = await upsertFacts(facts, load.id); await finishDataLoad(load.id, { inserted: res.written, updated: 0, suppressed: res.suppressed }, `Dade rows ${dade}`); console.log(`[srs] ${sourceId.padEnd(40)} ${metricKey.padEnd(28)} ${year} ${res.written} facts (${res.suppressed} suppressed)`); } } runMain(main);