Methodology
# Building Grade Methodology
## Status
**PROVISIONAL** — weights and cutoffs below are a spec-proposed starting
point (specs/005-building-grade-v0.md Ambiguities 1 and 3), approved by the
founder as part of this spec, but not yet empirically validated against real
buildings. Recalibration against `/audit/panel.json` (BACKLOG item 004) is
expected before or shortly after launch and does not require a new spec —
update this file and `packages/scoring/src/methodology.ts` together, and log
the change in DECISIONS.md per CLAUDE.md's memory discipline.
## Scope
Building Grade v0 uses **HPD open violations only** (class A/B/C). DOB
complaints, 311 heat/hot-water complaints, and bedbug filings are grade
methodology v1.1 (BACKLOG item 009) and are **not** included in this
version's formula — there is no placeholder field or zero-weighted stub for
them anywhere in `packages/scoring`.
## Inputs
- **Open violation counts by class** — source: the `hpd_violations` table
(BACKLOG item 003), filtered to `violation_status = 'Open'` (the exact,
live-confirmed Socrata string — no case-insensitive match), grouped by
`class` (A, B, C, or unclassified — see Edge cases below).
- **Building unit count** — source: **not yet ingested**. No PLUTO (or any
other) unit-count data source exists in this codebase today (spec 005
Ambiguity 4). A grade cannot be computed for a real building until a future
ingest (proposed: a PLUTO building unit-count ingest, a new BACKLOG item)
lands and is wired into `createGradeVerifier()`'s injected `GetUnitCount`.
## Formula
Each open violation contributes points by class:
| Class | Points per open violation |
| ----- | -------------------------- |
| A | 1 |
| B | 3 |
| C | 5 |
```
weightedPoints = (classA count × 1) + (classB count × 3) + (classC count × 5)
```
Violations with no recognized class ("unclassified" — see Edge cases) are
**excluded** from `weightedPoints`.
Normalization: points are scaled **per 100 units** rather than compared as a
raw per-unit ratio, so the resulting numbers are legible in round terms
rather than dominated by rounding noise on small buildings:
```
pointsPer100Units = (weightedPoints / unitCount) × 100
```
## Cutoffs
Grades are assigned by walking the table below in ascending order and
picking the first row whose ceiling is greater than or equal to a building's
`pointsPer100Units`:
| Grade | Points per 100 units |
| ----- | --------------------- |
| A | 0 (zero open violations, any class) |
| B | (0, 20] |
| C | (20, 60] |
| D | (60, 150] |
| F | (150, ∞) |
## Edge cases
- **Unknown unit count.** A building with an unknown, zero, negative, or
non-integer unit count shows **no grade** ("data unavailable") — never a
default "A". A grade must never be shown as possibly-wrong data.
- **Unrecognized/missing violation class.** Violations whose `class` field is
`null` or an unrecognized string are counted and shown (in `subScores`) for
transparency, but are **not weighted** into the score — an unknown class
cannot safely be treated as any specific hazard tier. This means a building
with a large unclassified count could display a rosier grade than its raw
violation count alone would suggest — a known methodology limitation, not
a bug.
- **Small buildings.** Because the formula normalizes per unit (scaled to
per-100-units), a handful of violations swings a small building's score
far more than the same handful would for a large building. No damping
floor is applied by default — this is an accepted tradeoff of per-unit
normalization, not a defect, and a candidate refinement for grade
methodology v1.1 (BACKLOG item 009).
## Sourcing and freshness
A building's grade is recomputed at data-refresh time from the latest
ingested HPD violation counts and unit count — never computed live,
per-request. Before a recomputed grade is published, it is independently
re-verified: a fresh re-derivation from source records must agree with the
stored value (the verification gate, BACKLOG item 016) before the grade is
shown to a visitor. A grade that fails this check is quarantined for review
rather than published.
## Display note (BACKLOG item 023)
Buildings with a unit count under 6 show a "small building — grade less
reliable" caveat on the dossier page. This is a **display-only** annotation
— the founder deferred any change to the Formula or Cutoffs sections above
until the ground-truth panel (item 004) calibrates real thresholds for small
buildings. `computeBuildingGrade()`'s weights, cutoffs, and unit-count
handling are unchanged by this note.