Source document

docs/design/01_inventory.md

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.

01 — Prototype Inventory (mwg1378/nyc-edu-data)

Prototype inventory — read-only audit; no prototype code was modified. Clone audited at main @ a4baa1a ("Editorial sweep: finish the remaining ~30 stories"), 157 tracked files, single branch, pushed 2026-05-31.

This is one of the design/review docs in docs/design/. The reference Python scaffold these docs cite lives in docs/design/reference-scaffold/ (scaffold paths like pipeline/... are relative to it); nyc-school-outcomes-spec.md is in this folder. See docs/design/README.md for the map.


1. Stack summary

LayerTechnologyEvidence
LanguageTypeScript (primary), some Python helpersrepo language = TypeScript; package.json; scripts/loaders/*.py
FrameworkNext.js 16.2.6 (App Router, React 19, Turbopack)package.json:24,27; app/ tree; next.config.ts
DB / ORMSupabase (Postgres) via Prisma 7.8 (@prisma/adapter-pg)package.json:15-17; prisma/schema.prisma; lib/supabase/*
Data accessServer components query Prisma directly; no JSON data files read by the sitelib/queries/schools.ts:2; JSON-read grep found none in app/,lib/
UIshadcn/ui + Tailwind v4 + @base-ui/react + lucide + sonnerpackage.json:18,22-23,29-33; components/ui/
AuthSupabase SSR (Google OAuth); most pages publicSPECS.md:230-239; lib/supabase/, app/auth/
Ingest libsxlsx, csv-parse, pg, zod, tsx, dotenvpackage.json:19,21,26,34,43,48
AI@anthropic-ai/sdk (editorial tooling; no runtime AI features in v1)package.json:14; SPECS.md:246-252
Testsvitest (3 unit specs only)vitest.config.ts; tests/unit/
Linteslint-config-next flat config (core-web-vitals + typescript)eslint.config.mjs
TSstrict, ES2017, bundler resolution, @/* aliastsconfig.json
DeployVercelvercel.json, next.config.ts
Hooks/CIlocal .githooks/pre-push runs unit tests only; no .github/ → no CI.githooks/pre-push; find .github → absent

Entry points / scripts (package.json:5-12): dev (next dev -p 3008), build (prisma generate && next build), start, lint (eslint), test (vitest), prepare (wires .githooks). Data pipeline is not in package.json; loaders run ad-hoc via tsx (e.g. tsx scripts/loaders/test-results.ts), gated by .env.local (DIRECT_URL).

⚠️ Stack mismatch with our scaffold. The prototype's analysis layer is TypeScript + a live Postgres DB. Our reference scaffold (reference-scaffold/) is Python (numpy/pandas/scipy/pyarrow, requirements.txt) emitting static JSON files the site reads. This is the single largest "does it integrate cleanly" question — carried into 03_integration_plan.md §Stack-fit.


2. Data flow (raw → site)

The prototype is DB-centric, not file-centric. There is no analysis_table artifact and no JSON export stage; computed columns are written back into Postgres and the site reads them live.

NYCDOE InfoHub xlsx / NYC Open Data CSV / Socrata / NYSED
        │  (fetchAndCache → data-sources/_cache, gitignored)         scripts/loaders/_fetch.ts:14
        ▼
[ingest+parse, one loader per source]  test-results.ts, demographics.ts, graduation-results.ts,
        │   normalizeDbn / normalizeYearLoose, isSuppressed                school-quality.ts, attendance.ts,
        ▼                                                                  nyc-school-survey.ts, nysed-ssec.ts, hs-directory.ts,
   Postgres (Prisma)  ── school_year_metrics (long format)                 nyc-od-schools.ts, nyc-od-charters.ts, dbn-beds-crosswalk.ts
        │              ── schools, school_year_demographics, survey_*
        │              ── data_loads (provenance row per run)         scripts/loaders/_lib.ts:30-62
        ▼
[derive / compute, in-DB]
   derive-admission-topic.ts   → schools.admission_category, topic_tags
   nycenet-comparison-groups.ts→ comparison_groups (DERIVED, K=40) + schools.comparison_group_id
   compute-proclivity.ts       → schools.proclivity_decile / proclivity_score
   compute-percentiles.ts      → school_year_metrics.comparison_group_percentile / citywide_percentile
   compute-composites.ts       → composite_* metric rows
   compute-survey-metrics.ts   → survey_* metric rows
        ▼
[site reads DB live via Prisma]   lib/queries/schools.ts
   app/ratings/outliers  (decile filter on comparison_group_percentile)
   app/ratings/movers    (raw cross-year value delta)
   app/schools/[dbn], /parents/*, /researchers/*, /stories/*

Sources, vintages & provenance:

  • Source catalog: SPECS.md:321-339; per-source notes stub at data-sources/README.md.
  • Test results pull years 2018–2025 from two fixed InfoHub xlsx URLs (scripts/loaders/test-results.ts:12-23); ingests grades 3–8 + "All Grades", both % Level 3+4 and Mean Scale Score (test-results.ts:154-182).
  • Provenance row per load: source_name, source_url, script_path, git_sha, row counts, notes (_lib.ts:30-62, prisma/schema.prisma:399-419). No source-file content checksum/sha256 is recorded; the cache is keyed by URL only (_fetch.ts:9-29).
  • Raw downloads are gitignored (.gitignore:32-46) — not committed.

3. Analysis-relevant module map

ConcernFile:lineWhat it does
Peer groups (KNN)scripts/loaders/nycenet-comparison-groups.ts:24-95,180-207K=40 KNN. Hard filters: same grade band + admission bucket (ADMISSION_BUCKETS :39-47). Weighted distance: Euclidean over 7 demographic shares (weight 1.0, :66-79) + topic Jaccard distance ×30 (weight 0.5, :81-95). Peers stored as JSON list in comparison_groups.methodology_notes (:193-204). All groups are DERIVED; NYCENET portal is not scraped (no bulk endpoint — :1-3).
Outlier percentilescripts/loaders/compute-percentiles.ts:60-78,141-156Midrank percentile (0..1) of a school's value within its stored peer set (needs ≥5 peers present, :148) and within all default-included schools (citywide). LOWER_BETTER inverted via sign (:133-134). Pure rank — no covariate adjustment of the outcome.
Outlier flagging (decile)lib/queries/schools.ts:166-172"Outlier" = top decile (comparison_group_percentile ≥ 0.9) or bottom decile (≤ 0.1). Threshold lives in the query layer, not the compute step.
Proclivity decilescripts/loaders/compute-proclivity.ts:17-35,92-115ntile(10) of the mean percentile rank across 3 "challenge" features (pctEconDis, pctEll, pctSwd). Stored on schools.proclivity_decile/score. Used only as a filter/sort in the outlier query (schools.ts:176-181,195-196) — never feeds the percentile/outcome computation.
Compositesscripts/loaders/compute-composites.ts:20-59,126-149Simple mean of constituent raw metrics (ES/MS academic, HS outcomes, teacher-voice); requires all inputs present. Built on raw rates, not residuals.
Survey rollupsscripts/loaders/compute-survey-metrics.ts:25-99,113-185Regex keyword-matches question text per (metric, respondent) independently each year (questions are year-versioned), then averages positive_pct. No cross-year wording/scale-continuity verification before rollup. Subgroup always ALL (:135-136).
Trends / moverslib/queries/schools.ts:272-318 + app/ratings/movers/page.tsx:45-51,110-113getYearOverYearMovers = raw to_value − from_value between two chosen years; requires both non-null + not suppressed. No within-year z, no comparability regime, no NextGen-break awareness. UI renders the raw delta.
Site read layerlib/queries/schools.tsgetOutliers (decile filter + sort + 1h memo, :147-270), getPeerGroupContext (:26-48), getYearOverYearMovers (:272-318). All Prisma/Postgres.
Admission/topic derivationscripts/loaders/derive-admission-topic.tsDerives admission_category + topic_tags (inputs to peer matching).
Ingest helpersscripts/loaders/_fetch.ts, _lib.ts, _bulk.ts, normalize.tsfetch+cache, DataLoad provenance + gitSha(), bulk upsert, year/DBN normalization.
Schemaprisma/schema.prismaSchool.proclivityDecile/Score (:173-174), admissionCategory/topicTags (:176-178), SchoolYearMetric.comparisonGroupPercentile/citywidePercentile (:289-290, indexed :301), ComparisonGroup (:221), DataLoad w/ gitSha (:399-419). Long-format facts per SPECS.md:106-122.

Methodology surface to users: app/methodology/page.tsx; the decile rule is the documented v1 method (SPECS.md:300, methodology/README.md:7). Planned methodology write-ups (methodology/README.md:16-24) are not yet authored.

Notable signal — the editorial side already prefers regression residuals. The team's own quant playbook says decile means are a piecewise-constant baseline that distorts the extremes and that "a continuous regression baseline … is the more defensible methodology" (docs/agents/quantitative-knowledge.md:38-47), warns that top-N residual lists are selected-on-outcome (:18-26), and recommends a 3-year rolling residual for stability (:28-36). The published stories speak in "proclivity-adjusted residual" / "gap above expectation" terms (docs/agents/plain-language.md:60,67; data/stories/answers.ts:1216) even though the code computes deciles. The spec's residual-z method formalizes language the project already uses editorially.


4. Contribution conventions (for Phase 5)

ItemFindingSource
Default branchmain (single branch; origin/HEAD → main)git branch -a
Branch protectionNonebranches/main/protection returns 404. main is technically pushable directly; the plan mandates branch+PR anyway.gh api …/branches/main/protection
Open PRs / IssuesNone (clean slate; no in-flight work to collide with)gh pr list, gh issue list
Visibility / licenseprivate, no license file (license decision deferred, spec §9.10)gh api repos/…; nyc-school-outcomes-spec.md:338
Lint/formateslint flat config (eslint-config-next: core-web-vitals + typescript). No Prettier config; no separate formatter.eslint.config.mjs
Type checkingstrict TS; not run in pre-push hooktsconfig.json:7
Test runnervitest; specs in tests/**/*.test.ts, @→repo root alias; node envvitest.config.ts; existing: tests/unit/{format,normalize,routes}.test.ts
Pre-push gate.githooks/pre-push runs vitest run tests/unit/ only (no lint/typecheck/build). Wired via prepare script..githooks/pre-push; package.json:11
CINone (.github/ absent). All gating is local.find .github
CODEOWNERSNonefind (absent)
Commit styleSentence-case, scope-prefixed imperative summaries; heavy "Editorial sweep: …" series; no Conventional-Commits prefixes (feat:/fix:).git log --oneline -8
Agent config.claude/ and .codex/ hooks present (editorial agent tooling)repo root
Data hygieneraw xlsx/csv, _cache, .env*, scrape caches all gitignored.gitignore:9-46
Test fixturessample CSVs allowed (!data-sources/**/sample/*.csv).gitignore:38

House-style implications: new code matching the prototype is TypeScript under scripts/loaders/ (or lib/), with vitest tests under tests/. There is no CI to extend — only the local pre-push hook — so any new gate (golden/determinism, reconcile) must be added as a vitest spec (TS) or a new hook/CI workflow. A Python pipeline (the scaffold) would introduce a second toolchain with no existing runner — see 03_integration_plan.md.


5. README staleness flag

README.md:5 still says "Status: Spec phase. No code yet." — stale; the repo is a fully built Next.js app with a complete loader pipeline and ~30 published stories. Worth correcting in an early housekeeping PR.