Source document

scripts/loaders/miami/srs_assessments.ts

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

// 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<string, { metricKey: string; year: string }> = {
    "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);