# NJDOE SPR Discipline and Safety

```toml ergo
[dataset]
ergo = "0.4"
slug = "spr-discipline-safety"
title = "NJDOE SPR Discipline and Safety"
publisher = "NJ Department of Education, Office of Performance Reports"
subject = "https://www.nj.gov/education/spr/"
source_url = "https://www.nj.gov/education/sprreports/download/DataFiles/2024-2025/Database_DistrictStateDetail.xlsx"
pitfall = "These are separate per-edition records of reported incidents and responses, not a combined safety score or evidence of cause, effectiveness, or budget performance; NJDOE states its own 2019-20 and 2020-21 discipline data are not comparable with adjacent years."
status = "live"
confidence = "A"
updated = "2026-08-03"
implementation = "https://github.com/lavallee/njschooldata"

[dataset.coverage]
period = "2024-25 School Performance Report edition for the conformed marts; the eight editions that carry a district disciplinary-removals sheet, 2017-18 to 2024-25, for the removal history"
grain = "district headline x metric; district x HIB nature x metric; district x All Students publisher row x metric; district x student group x metric and district x grade x metric; district x edition x removal measure x unit"
entities = "666 district reporting units; 576 of 587 rendered budget districts match by exact code"

[dataset.access]
keys = ["school_year", "district_code", "tab_id", "dimension_label", "metric_id"]
```

## Consumer question and boundary

AQ-062 adds one product-owned question to the district report:

> What did the 2024-25 SPR record about this district's reported incidents,
> HIB investigations, removals, police notifications, and arrests before a
> reader treats those records as proof of safety, cause, or budget
> performance?

Aquifer owns official-workbook custody, exact source literals and locators,
publisher identifiers and defects, three source grains, value states,
immutable releases, and offline snapshots. This repo owns the district-report
question, the bounded marts, exact-code join, wording, placement, static UAT,
and recovery through retained pins.

On 2026-07-30 a second product-owned question joined it, on the district
profile rather than the budget report:

> Which students in this district were suspended, referred to police and
> arrested — and where New Jersey did not say, what kind of not-saying was it?

The panels do not rank districts, construct a safety score, add incident-type
counts, treat a police notification as an arrest, reverse engineer a
suppressed denominator, compute a disproportionality ratio or risk index
between two student groups, or align the SPR record to a later UFB budget
year. Headline incident rows, HIB-nature rows, and the publisher's
`All Students` student-group rows remain separate marts. Student-group and
grade rows are now projected, in their own additive table, at district level
only — the release's school coverage is 259 school codes and nothing renders
on a school profile.

## Why the count/percent choice went to the percent

Both a headcount and a percentage exist for every removal, police-notification
and arrest measure. The surface leads with the **percentage**, and the reason
is measured rather than aesthetic.

The percentage is a **within-group rate**: the share of that group's own
students, against that group's own denominator, not the group's share of the
district total. South Orange-Maplewood's Black students carry 25 students and
1.60%, which implies about 1,563 Black students — the district's own Black
enrollment, not its total.

And the two columns are not suppressed alike. For male and female students
NJDOE withholds 12,467 of 12,654 district headcounts each (98.522%) while
withholding only 2,040 of the matching percentages (16.121%). In 8,328 male
and 8,506 female cells the withheld headcount sits beside a **published exact
zero** percentage. A count-led surface would print "withheld" over cells where
the state plainly said nobody, and would lose sex entirely in 98.5% of
districts. A rate-led surface keeps it in 83.9%. The headcount is still shown
wherever NJDOE published it, and its absence is stated where it did not.

## How holey the student-group surface actually is

Measured on what the module renders — three measures across twelve student
groups in 666 districts, 23,976 cells — reading the headcount and the
percentage together:

| state | cells | share |
| --- | ---: | ---: |
| a published rate | 7,167 | 29.89% |
| a published exact zero | 7,392 | 30.83% |
| `<5` — headcount masked below five (often a zero) | 2,253 | 9.40% |
| withheld outright | 7,164 | 29.88% |

Per student group, the withheld share runs inversely to group size:

| student group | rate | zero | `<5` | withheld | withheld % |
| --- | ---: | ---: | ---: | ---: | ---: |
| Non-Binary/Undesignated Gender | 3 | 30 | 6 | 1,959 | **98.05** |
| American Indian or Alaska Native | 39 | 125 | 31 | 1,803 | **90.24** |
| Asian, Native Hawaiian, or Pacific Islander | 342 | 533 | 201 | 922 | 46.15 |
| Two or More Races | 340 | 571 | 221 | 866 | 43.34 |
| Black or African American | 595 | 585 | 281 | 537 | 26.88 |
| Female | 755 | 850 | 6 | 387 | 19.37 |
| Male | 815 | 790 | 6 | 387 | 19.37 |
| White | 713 | 781 | 366 | 138 | 6.91 |
| Economically Disadvantaged Students | 789 | 738 | 366 | 105 | 5.26 |
| Hispanic/Latino | 771 | 798 | 384 | 45 | 2.25 |
| Students with Disabilities | 798 | 800 | 385 | 15 | 0.75 |
| All Students | 1,207 | 791 | 0 | 0 | 0.00 |

Grade is nearly complete by comparison — 0.60% to 2.93% withheld — but that
completeness is mostly published zeros, not published rates: Grade PK is
78.6% zero and 0.33% rate. **The early grades are a substantive finding about
who gets suspended, not a data gap.**

By district enrollment band, on the same surface:

| enrollment | districts | cells | withheld % | rate % |
| --- | ---: | ---: | ---: | ---: |
| 0–249 | 81 | 2,916 | 51.27 | 6.31 |
| 250–499 | 118 | 4,248 | 46.45 | 12.95 |
| 500–999 | 134 | 4,824 | 40.61 | 23.32 |
| 1,000–2,499 | 173 | 6,228 | 36.71 | 38.34 |
| 2,500–4,999 | 91 | 3,276 | 29.52 | 47.56 |
| 5,000–9,999 | 52 | 1,872 | 30.02 | 53.74 |
| 10,000+ | 16 | 576 | 27.43 | 60.24 |

In 182 of 666 districts no student group carries a published suspension rate
at all. In most of those the state published zeros rather than withholding —
which is why the surface must render a printed zero and a withheld cell as
visibly different things.

## Exact offline pin and mart

The committed pin is
`contracts/aquifer/nj-spr-discipline-safety.lock.json`. It names **two**
artifacts of one release, and the consumer verifies both before it reads
either:

- release: `nj-spr-discipline-safety-aq090-all-editions`;
- manifest SHA-256:
  `1759b1aadeb376ac964bbf5a5ad34ff10a0c33b37abcb160c8d01e5a9a3405f9`;
- conformed 2024-25 artifact: SHA-256
  `ccf481fbb325423734063f86b132de10246ea68f6723ba994cc112090473f831`,
  `2,355,728,384` bytes, 207,547 rows;
- all-editions artifact: SHA-256
  `781103e1e143f2bf1999e368266c46307bf1ed3b8292219a8acc3f6b836a673b`,
  `719,687,680` bytes, 637,395 rows across 37 tab-editions.

The pin moved on 2026-08-01 from `nj-spr-discipline-safety-aq062-refresh2`,
which no longer exists on disk: the dataset's rollback strategy is a pointer
swap retaining one prior release, and aq062-refresh2 has aged out.

**The 2024-25 artifact's SHA-256 changed across that move and not one published
value did.** Its byte count and row count are identical; what moved is a
run-scoped suffix on every `capture_id` — `…-f208ae8e` became `…-7f216dfb` —
while the workbook content hash behind it, `1445a4279ee2…`, did not move at
all. Every consumer projection carries `capture_id`, so every full-row hash
below moved with it, which makes the full-row hash useless on its own as proof
that adopting the new release was additive.

So the adapter asserts a second hash per table: the same rows in the same
order with `capture_id` and `source_row_id` dropped. Those were measured on the
pre-move mart before the change was written and are asserted unchanged after
it. They are the actual proof.

| Consumer table | Lineage-independent row SHA-256 |
| --- | --- |
| `discipline_safety_district_headline` | `eea9a609f0081d2885f67e7472b601cacac1293deef79ca58f43ffd63579e19d` |
| `discipline_safety_district_hib_nature` | `f7611b79f27779bc5ad6dad8f1b6645dec6f2aad16a4ae1b7f87066c3d7a5204` |
| `discipline_safety_district_all_students` | `feeec6a63bf27b1087f8ece67f15fb56df193cb291cb98a2b93df9129189e33f` |
| `discipline_safety_district_group_grade` | `7f4a33d6d023835059c47c51eaf71a1e9450e16774c14e2e20795d97a5728fb3` |
| `discipline_safety_metric_catalog` | `0fc5c2d84b10c96e40f7ac081c457d262194fea280a4faba942690c157eb5e13` |

The metric catalog is the one mart carrying no lineage column, and its full-row
hash is unchanged across the move — which is itself a small proof that
`capture_id` is the only thing that moved.

The consumer verifies the entire pin before building a fresh family file.
It checks all 207,547 physical rows, 2,344,237 typed values, 54 metric
definitions, six tab populations, and the complete producer value-state
receipt. It then projects only district records needed by the owned question,
validates the exact owned-table set, and atomically replaces the family file:

| Consumer table | Rows | Deterministic row SHA-256, under aq090 |
| --- | ---: | --- |
| `discipline_safety_district_headline` | 8,658 | `2e275ed038a01845e3e7099d1525184bdc8f0337a58b3a906cf3bed8ef933784` |
| `discipline_safety_district_hib_nature` | 15,984 | `ad5bcb353ae9a23ed41c536b0ebff25256ecfba21f958e2f9b1dbd5cc7eb5e5a` |
| `discipline_safety_district_all_students` | 25,308 | `b53bf15fbe7c40435cbe6af91a1b3d623e4595be02045e34251f20c722bb4056` |
| `discipline_safety_district_group_grade` | 570,342 | `963428c95a1f6e3a8494d5297911950bb1c4efc1fde56dde3d871da141eaf89a` |
| `discipline_safety_metric_catalog` | 54 | `0fc5c2d84b10c96e40f7ac081c457d262194fea280a4faba942690c157eb5e13` |
| `discipline_safety_district_removal_history` | 65,701 | `6a64d2b59ec18e866519d47435947af4afe7b2220d00803870bb8bc49c7687ca` |
| `discipline_safety_edition_coverage` | 60 | `2e28d8303a9f9c16dab02c4242f6307d94dc849db0f8535b6b92c788028ed941` |
| `discipline_safety_edition_identity` | 9 | `4bb37ccabc9910e8bdc874d678a9ffa487e76feeda245c01f270597d84be337c` |

The first five hashes were re-baselined on 2026-08-01 with the release move.
They pin the exact release; the lineage-independent hashes above pin the
values.

## The record over time

`discipline_safety_district_removal_history` is the eight-edition district
all-students removals record, read from the second artifact. 65,701 rows: 668
districts plus the published statewide row, eight editions, seven measures,
count and percent.

Nothing in it is conformed except the column names, and that mapping is
`REMOVAL_COLUMN_MAP` in the adapter, checked per edition against the sheet's
own declared measure columns before a row is written.

| what | value |
| --- | ---: |
| rows | 65,701 |
| editions | 8, 2017-18 to 2024-25 |
| measures | 7, of which 5 run all eight editions |
| reported values | 23,026 |
| published exact zeros | 31,575 |
| non-numeric literals | 11,100 — `*` 10,338 and `N` 762 |
| rows where the figure is the district's own row | 52,351 |
| rows where it is a student-group member | 13,350 |

`discipline_safety_edition_coverage` (60 rows) is the publisher-side register
of what exists at district grain: 37 collected tab-editions, 17 not collected
in that edition, and 6 that the 2015-16 edition does not publish at this grain
at all because it ships a school workbook and no district workbook. Each
absence carries the producer's stated reason, so the three are never collapsed
into one "missing".

`discipline_safety_edition_identity` (9 rows) carries the district code
resolution per edition and the evidence-window caveat verbatim.

The headline mart retains 4,637 reported and 4,021 exact-zero values. The
HIB-nature mart retains 6,713 reported, 8,605 exact-zero, and 666 structural
`n/a` values. The `All Students` mart retains 8,401 reported and 16,907
exact-zero values.

All five pre-existing tables' hashes are asserted by the adapter itself
(`LEGACY_ROW_SHA256`) alongside the lineage-independent `VALUE_ONLY_SHA256`, so
both the 2026-07-30 widening and the 2026-08-01 release move are provably
additive: neither can change a byte of what was already there.

The widened `discipline_safety_district_group_grade` table holds 325,103
exact-zero, 62,047 reported and **183,192 suppressed** values — every
district-level suppression the pinned artifact records. Before it existed the
consumer warehouse had a suppression rate of exactly 0.000%, because the three
narrow marts kept precisely the slices NJDOE never suppresses. That was an
artefact of the projection, not a property of the data.

It is deliberately narrower in columns than its siblings: at 570,342 rows the
repeated metric label, definition, publisher defect and source URL would add
several hundred megabytes of identical text, so those live once per metric in
`discipline_safety_metric_catalog`. What is never trimmed is `value_state` and
`reported_literal`, carried verbatim so `0`, `0.00%`, `*`, `<5` and `n/a`
remain five distinguishable publisher acts.

Structural `n/a` does not occur in this slice — all 666 of the release's
district-level structural absences sit in the HIB-nature mart, under
`No Identified Nature`. The adapter asserts that as an exact expectation rather
than assuming it.

## Issues

### Two suppression markers that do not mean the same thing

```toml ergo
[issue]
id = "two-suppression-markers"
title = "Suppressed cells carry either `*` or `<5`, and `<5` bounds the value where `*` says nothing at all"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "Release-wide the 750,782 suppressed values split 661,604 `*` and 89,178 `<5`. In the district student-group and grade slice the split is 148,999 `*` and 34,193 `<5`. No suppressed cell carries a numeric value."
misuse = "Coalescing both markers to NULL or to a single 'suppressed' label. `<5` bounds the headcount below five; `*` states only that the publisher withheld the cell. Collapsing them discards a bound the publisher chose to give. Note the bound runs from zero, not one — see less-than-five-includes-zero."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal", "value_state"]
rows = "value_state = 'suppressed'"
```

The mart carries `reported_literal` verbatim so the two stay separable, and the
profile module renders them as two different words — **Fewer than 5** and
**Withheld**.

### `<5` is a headcount marker only

```toml ergo
[issue]
id = "less-than-five-is-count-only"
title = "`<5` never appears on a percentage metric; the percentage partner of a `<5` headcount is always a bare `*`"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "All 89,178 `<5` literals in the release sit on the students-or-incidents unit; the percent unit carries only `*` (322,625 cells). Joining each count metric to its percent partner on the same source row gives the pair ('<5','*') in every student group and every grade."
misuse = "Resolving a cell from its percentage alone. The percentage says only `*`, so a reader who never looks at the headcount column throws away the below-five bound on 34,193 district cells."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal", "unit"]
rows = "reported_literal = '<5'"
```

Every cell on the profile module is resolved from the count/percent **pair**
rather than from either column alone. That is the only way the one-to-four
bound survives to the page.

### `<5` includes zero, so it is not evidence that anything happened

```toml ergo
[issue]
id = "less-than-five-includes-zero"
title = "The `<5` marker bounds a headcount at zero to four, not one to four — most `<5` cells are masked zeros"
effect = "corrupts"
core = true
type = "suppression"
status = "mitigated"
detection = "Grades are mutually exclusive and partition a district, so the published grade headcounts plus the `<5` grade cells must reconcile to the All Students headcount. Among the 574 districts with a published All Students any-suspension count and no `*` grade cell, 96 have an All Students total smaller than their number of `<5` grade cells — district 0070 suspended one student in total and carries ten `<5` grade cells, district 0080 one student and nine. Those cells cannot all contain at least one student, so at least nine of ten are exactly zero."
misuse = "Reading `<5` as 'between one and four students', which turns a masked zero into an incident. In a small district that inverts the whole grade table: ten grades each reading 'fewer than 5' suggests discipline spread across every grade when the district suspended one child."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["reported_literal"]
rows = "reported_literal = '<5'"
```

The module says **0 to 4 students** rather than "between one and four", and its
legend calls the marker a masked headcount that is often a zero. This is the
mirror image of the published-zero problem: there, a zero gets read as a gap;
here, a gap gets read as an incident.

### The small-cell rule is applied inconsistently

```toml ergo
[issue]
id = "small-cell-rule-is-inconsistent"
title = "NJDOE masks 34,193 small district headcounts as `<5` but publishes 13,939 headcounts of one to four outright, so a published rate can rest on a single student"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "In the district student-group and grade slice, 13,939 reported headcounts fall between 1 and 4 while 34,193 comparable cells are masked `<5`; 15,079 reported headcounts are 5 or more. Nine districts publish an American Indian or Two or More Races rate of exactly 10.00% off a headcount of 1. On the three-measure student-group surface, 1,960 of 7,167 published rates (27.3%) rest on a headcount below five."
misuse = "Treating a published rate as evidence of a pattern. One student in a group of ten prints as 10%, which is arithmetic and not a finding; and treating the `<5` mask as a guarantee that every small cell is protected."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["numeric_value"]
rows = "value_state = 'reported' AND unit = 'students-or-incidents' AND numeric_value < 5"
```

The module states a floor and enforces it in the chart: a rate whose published
headcount is one to four keeps its table row, with the headcount beside it, and
is shown on the dot plot as a labelled row with no dot. Every rate that does
carry a dot is shown with its headcount, or with a note that the headcount was
withheld.

### Male and female headcounts are withheld while their rates are published

```toml ergo
[issue]
id = "sex-count-withheld-rate-published"
title = "For Male and Female alone, NJDOE suppresses 98.522% of district headcounts and only 16.121% of the matching percentages"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "Male and Female each carry 12,654 district count cells and 12,654 percent cells. 12,467 count cells are suppressed against 2,040 percent cells. Every other student group suppresses the two columns at identical rates to three decimals. Pairing each count with its percent on the same source row, 8,328 Male and 8,506 Female cells carry a suppressed `*` count beside a published `0.00%` percentage. `spr.discipline.arrests.any-count` is suppressed for 659 of 666 districts for both."
misuse = "Building a student-group surface on headcounts. Sex becomes unreportable in 98.5% of districts, and in the 8,328/8,506 zero-percentage cells a count-led reading prints 'withheld' over a cell where the publisher stated an exact zero."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state", "reported_literal"]
rows = "conformed_dimension_label IN ('Male','Female')"
```

The adapter asserts the four suppression counts as a fail-closed expectation
(`EXPECTED_SEX_ASYMMETRY`) so a refreshed release that changes NJDOE's
disclosure behaviour breaks the build rather than the page. The module leads
with the percentage, shows the headcount wherever it exists, and says in place
that the number was withheld where it does not.

### Suppression runs inversely to student-group size

```toml ergo
[issue]
id = "suppression-tracks-group-size"
title = "The smallest student groups are withheld most: 98.522% of Non-Binary cells and 92.327% of American Indian cells against 16.603% for Students with Disabilities"
effect = "misleads"
core = true
type = "coverage"
status = "mitigated"
detection = "Each student group carries 25,308 district cells. Suppressed counts run Non-Binary/Undesignated Gender 24,934; American Indian or Alaska Native 23,366; Male 14,507; Female 14,507; Asian/NHPI 14,068; Two or More Races 13,388; Black or African American 9,920; White 5,614; Economically Disadvantaged 5,232; Hispanic/Latino 4,572; Students with Disabilities 4,202; All Students 0. Non-Binary carries 14 reported values of 25,308 (0.055%) and American Indian 194 (0.767%)."
misuse = "Rendering only the cells that were published. Because the holes are patterned by group size, a chart of the surviving cells redraws discipline as a White, Hispanic, Black, economically-disadvantaged and disabled-student phenomenon — those being the groups whose cells survive. Equally: computing any ratio, risk index or 'X times more likely' statement between two groups censored at different rates."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state"]
rows = "dimension_kind = 'student-group'"
```

Every group keeps its row on the module, in the table and on the dot plot, with
the reason for its absence written where the value would be. No ratio between
two groups is computed anywhere on the site.

### A published zero is not a gap, and the early grades are mostly zeros

```toml ergo
[issue]
id = "published-zero-is-not-a-gap"
title = "`0` and `0.00%` are exact publisher zeros, not missing values, and they are the majority state in the early grades and the small districts"
effect = "corrupts"
type = "suppression"
status = "mitigated"
detection = "The district student-group and grade slice holds 325,103 exact-reconciliation-zero cells (`0` 154,130 and `0.00%` 170,973) against 183,192 suppressed and 62,047 reported. Grade PK is 78.6% published zero and 0.33% published rate. Across the whole district level, 97.733% of cells in districts under 250 students are either a suppression marker or a published zero, and the Pearson r between log10 enrollment and district suppression rate is only -0.2756 (n=665) — suppression is near-flat at 29-31% above 250 students while the reported rate rises 13.7x across the range."
misuse = "Rendering a published zero as 'no data', or a `*`/`<5` as zero. The two errors reverse a finding in opposite directions. Also: writing that small districts are censored more heavily — they are not; they are recorded as zero rather than published as a number."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
columns = ["value_state", "reported_literal"]
rows = "value_state IN ('exact-reconciliation-zero','suppressed')"
```

The module gives a printed zero its own word (**None**, with "the state
published a zero" beneath it) and its own styling, distinct from **Withheld**
and from **Fewer than 5**.

### Three dimensions exist in the schema and are effectively never published

```toml ergo
[issue]
id = "dimensions-effectively-never-published"
title = "Non-Binary/Undesignated Gender, American Indian or Alaska Native and Grade PK are present in every district row and almost never carry a value"
effect = "misleads"
type = "coverage"
status = "mitigated"
detection = "District-level reported cells: Non-Binary/Undesignated Gender 14 of 25,308 (0.055%), and 34 of 120,650 across all organization levels (0.028%); American Indian or Alaska Native 194 of 25,308 (0.767%); Grade PK 22 of 19,228 (0.114%). On the three-measure module surface Non-Binary is withheld in 98.05% of cells and American Indian in 90.24%."
misuse = "Reading these rows as zero, or as evidence that these students are never disciplined. The rows are withheld, not empty."

[issue.scope]
tables = ["discipline_safety_district_group_grade"]
rows = "conformed_dimension_label IN ('Non-Binary/Undesignated Gender','American Indian or Alaska Native','Grade PK')"
```

### The arrest columns are named after police notifications

```toml ergo
[issue]
id = "arrest-columns-are-named-police"
title = "The source fields behind the arrest measures are named PoliceCount and PolicePercent while the published layout defines them as all incidents leading to arrest"
effect = "misleads"
type = "definitional"
status = "mitigated"
detection = "metric_definitions carries publisher_defect = 'Source field says Police; layout defines all incidents leading to arrest.' on spr.discipline.arrests.any-count and spr.discipline.arrests.any-percent, and 'Source field is OtherIncidents; layout defines a total unique count.' on spr.discipline.police-notifications.unique-count."
misuse = "Reconciling the arrest column against the separate police-notification column, or treating one as a subset of the other. They are different tabs with differently-named source fields, and the site never adds or nets them."

[issue.scope]
tables = ["discipline_safety_metric_catalog", "discipline_safety_district_group_grade"]
columns = ["publisher_defect"]
rows = "metric_id LIKE 'spr.discipline.arrests.any-%' OR metric_id = 'spr.discipline.police-notifications.unique-count'"
```

The defect is carried into `discipline_safety_metric_catalog` and printed on the
module beneath the table, as published and unreconciled.

### These are reported incidents, so they measure reporting practice too

```toml ergo
[issue]
id = "reported-incidents-measure-reporting"
title = "Every measure here is an administrative record of what a district recorded and referred, so a diligent recorder looks worse than a district that handles the same behaviour informally"
effect = "misleads"
core = true
type = "definitional"
status = "open"
detection = "All 54 metrics carry value_origin = 'reported' and universe text describing publisher-reported counts and percentages. There is no independent incident audit in the file and no adjustment for reporting practice."
misuse = "Reading any of these numbers as behaviour, crime, safety, school quality or the effect of a budget decision; combining removals, police notifications and arrests into a single index or safety score; or ranking districts on any of them."

[issue.scope]
tables = ["discipline_safety_district_group_grade", "discipline_safety_district_headline", "discipline_safety_district_all_students"]
```

Stated on the module beside the numbers rather than in distant methodology
text, and the three measures are never combined.

## Issues in the record over time

The 2026-08-01 pin move added the second artifact — the source-shaped census of
all 37 tab-editions, 2016-17 through 2024-25 — and with it eight editions of
the district removals sheet. Everything below is a way that eight-edition
record can be drawn as continuous when it is not.

### NJDOE disowns two consecutive editions, and names a different cause for each

```toml ergo
[issue]
id = "covid-editions-are-not-comparable"
title = "NJDOE prints, inside the 2019-20 and 2020-21 workbooks, that their discipline data may not be comparable with adjacent years"
effect = "misleads"
core = true
type = "measurement"
status = "mitigated"
detection = "Both editions ship a notes sheet inside the workbook ('Important 2019-2020 Notes', 'Important 2020-2021 Notes') carrying the caution against each of four discipline elements. Neither sheet is linked from the download page and nothing in the values hints that it exists. The magnitude is visible in the values: statewide any-suspension counts run 55,971 in 2018-19, 39,646 in 2019-20, 4,644 in 2020-21, then 52,135 in 2021-22."
misuse = "Drawing a trend line through 2019-20 and 2020-21, or reading the fall in those two years as a change in student behaviour or a policy success. Schools were shut for part of spring 2020 and much of 2020-21; the fall is very largely a fall in the number of children in a building."
instead = "Break the series at 2019-20 and 2020-21 and print the publisher's own sentence for each, separately — the two editions state different causes, spring 2020 closures and then the pandemic as a whole, and collapsing them into one COVID note loses that."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
years = ["2019-2020", "2020-2021"]
```

The profile chart's line stops at 2018-19 and resumes at 2021-22. The two
editions are drawn as hollow marks inside a labelled band, and NJDOE's two
sentences are quoted in full directly beneath the chart — where the reader
meets those points, not in a footer and not on this page alone. The exact
wording is recorded in the validation block at the end of this section.

### `N` is a sixth literal and no edition that uses it published a legend

```toml ergo
[issue]
id = "an-unlegended-n-marker"
title = "Five editions print a bare `N` in place of a number and none of them says what it means"
effect = "misleads"
type = "coding"
status = "mitigated"
detection = "`N` appears in 762 district removals values across 2017-18 through 2021-22 and in zero values from 2022-23 onward. In 2018-19, 36 districts carry an entire removals row of `N` with no number in it anywhere; elsewhere it sits on a percent beside a published `0` count. The producer's own sheet-by-sheet check of all ten editions records that 2015-16 through 2018-19 carry 'no notes, legend, cover or reference sheet of any kind'."
misuse = "Reading `N` as zero, which invents a district that suspended nobody, or folding it into the same 'withheld' bucket as `*`, which claims to know it is a suppression."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
columns = ["reported_literal"]
years = ["2017-2018", "2018-2019", "2019-2020", "2020-2021", "2021-2022"]
```

Carried verbatim into the mart and rendered on the profile as the literal `N`
with the sentence "the state printed N and never said what it means". It is
never plotted and never counted as a zero.

### The all-students figure changes from a row into a dimension member

```toml ergo
[issue]
id = "all-students-changes-row-shape"
title = "Through 2022-23 the district's suspension figure is its own row; from 2023-24 it is a member of a student-group list, named `Districtwide` and then `All Students`"
effect = "breaks"
type = "format"
status = "mitigated"
detection = "The removals sheet has no dimension column at all in 2017-18 through 2022-23 (672 district rows per edition) and a `StudentGroup/GradeLevel` column from 2023-24 (15,702 rows) and `StudentGroupGrade` in 2024-25 (15,035). The all-students label is the empty string, then `Districtwide`, then `All Students` — and the State workbook's copy is `Statewide` in 2023-24 only."
misuse = "Reading the all-students figure positionally across the boundary. A consumer that expects one row per district gets an empty result for 2023-24 and 2024-25, which on a trend renders as a collapse to zero in exactly the two most recent editions."
instead = "Match on the label set, not on position — and do not draw a break there. The quantity did not move: NJDOE prints it under the same column name, `AnySusp_Count`, in 2017-18 and again in 2024-25."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
columns = ["row_scope", "dimension_label"]
```

### One edition renames every removal column and the next one changes back

```toml ergo
[issue]
id = "one-edition-renames-the-removal-columns"
title = "2023-24 alone spells the removal columns `#StudentsAnySuspension` and `%StudentsAnySuspension`; every other edition, before and after, uses `AnySusp_Count` and `AnySusp_Pct`"
effect = "breaks"
type = "format"
status = "mitigated"
detection = "Seven of the eight removals sheets declare the same measure-column names. The 2023-24 sheet declares ten differently-spelled ones whose text names the same quantities in words. 2024-25 reverts to the 2017-18 spelling."
misuse = "Treating the rename as a new measure and starting the series at 2024-25, or mapping the columns by position rather than by name."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
columns = ["source_column"]
years = ["2023-2024"]
```

The all-editions artifact is the source-shaped layer and states in its own
metadata that it "asserts no cross-edition metric identity". The mapping is
therefore made in this repo, written out as `REMOVAL_COLUMN_MAP`, and each
edition's declared measure-column set is checked against it before a row is
written. The build fails rather than dropping a measure: it did, on the first
run, because 2024-25 no longer publishes the arrest pair.

### Two measures stop after 2022-23 and one of them does not come back

```toml ergo
[issue]
id = "removals-sheet-lost-two-measures"
title = "`Arrest_Count`/`Arrest_Pct` and `SchoolDaysMissed_OSS` are on the removals sheet through 2022-23 and on no sheet at all afterwards"
effect = "misleads"
type = "availability"
status = "mitigated"
detection = "Five of the seven removal measures run all eight editions; arrest and school days missed run six. Arrests reappear on a separate sheet that begins in 2023-24 and names its columns `ArrestedCount`/`ArrestedPercent` in that edition and `PoliceCount`/`PolicePercent` in the next. School days missed appears nowhere after 2022-23."
misuse = "Joining the removals-sheet arrest column to the later arrests sheet to make an eight-year arrest series. They are two differently-named columns on two different sheets, and the second one's own field names say police notification."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
columns = ["measure_key"]
```

The profile prints the span of every measure from the data rather than
rendering the short ones silently, and states in words that no arrest series is
drawn.

### Suppression of the district row stops after 2021-22

```toml ergo
[issue]
id = "district-row-suppression-stops-in-2022-23"
title = "NJDOE withholds the district all-students suspension figure in roughly 130 to 240 districts per edition through 2021-22, and in none from 2022-23"
effect = "misleads"
type = "suppression"
status = "mitigated"
detection = "`*` on the matched-district any-suspension percent runs 133, 126, 130, 242, 132 across 2017-18 to 2021-22 and 0 in 2022-23, 2023-24 and 2024-25. The 2020-21 peak is the closure year. Commission 040 measured the consumer mart at 0.000% suppressed because it held 2024-25 only — the district row genuinely is never suppressed in the current edition."
misuse = "Reading a district whose early years are `*` as a district whose suspensions rose, when what rose is the publisher's willingness to print the number. 182 of 666 districts publish no student-group suspension rate at all in 2024-25, and the same small districts are the ones withheld here."

[issue.scope]
tables = ["discipline_safety_district_removal_history"]
years = ["2017-2018", "2018-2019", "2019-2020", "2020-2021", "2021-2022"]
```

### The student groups were redefined between the only two editions that have them

```toml ergo
[issue]
id = "student-groups-were-redefined-in-2024-25"
title = "The student-group breakdown exists in two editions and they do not share a category list"
effect = "misleads"
core = true
type = "definitional"
status = "mitigated"
detection = "2023-24 publishes `Asian` and `Native Hawaiian or Pacific Islander` as separate groups; 2024-25 publishes one `Asian, Native Hawaiian, or Pacific Islander`. `Hispanic` becomes `Hispanic/Latino`, `Districtwide` becomes `All Students`, and two labels change only in case. Seven labels are unchanged, and 1,722 of their 3,430 possible district cells (50.2%) carry a published rate in both editions. No edition before 2023-24 carries a student-group dimension at any grain."
misuse = "Drawing a two-point trend, a slope chart, or an arrow between the two editions — which is the form `vizier` returns for two time points and many categories, and the wrong one here. Also: adding 2023-24's Asian and Native Hawaiian or Pacific Islander rows to match 2024-25's merged category, which cannot be done wherever either side is withheld."
instead = "Show the current edition in full and say in words that the record is two years long and crosses a redefinition. When a third edition lands on the 2024-25 categories, this becomes a series."

[issue.scope]
tables = ["discipline_safety_district_group_grade", "discipline_safety_district_removal_history"]
years = ["2023-2024", "2024-2025"]
```

The profile module renders 2024-25 in full, renders no by-group trend at all,
and carries a *What is not drawn here* block naming each of the four label
changes. The refusal is on the page rather than only in this registry, because
a reader who cannot see why there is no chart will assume there is no data.

### District identity is only evidenced from 2020-08-11

```toml ergo
[issue]
id = "identity-evidence-starts-in-2020"
title = "For the four editions before 2020-08-11 a resolving district code is a verified code, not a verified district"
effect = "context"
type = "identity"
status = "mitigated"
detection = "The producer's identity register marks 2016-17 through 2019-20 as outside the evidence window and carries the caveat verbatim on each. District match rates are near-identical either side of the boundary (0.799 to 0.808), so the match rate is evidence about code stability rather than about entity continuity."
misuse = "Reading an eight-edition district line as a promise that the same schools sat inside the district throughout. New Jersey merges and regionalizes districts, and this file carries no district name at all in the source-shaped layer, so nothing here can confirm or deny a change."

[issue.scope]
tables = ["discipline_safety_edition_identity"]
years = ["2016-2017", "2017-2018", "2018-2019", "2019-2020"]
```

The register is projected whole, caveat included, and the profile prints it
under the chart on every district whose series reaches back before the window.

### What NJDOE says about these two editions, in its own words

```toml ergo
[validation]
date = "2026-08-01"
method = "Read the notes sheet inside each of the eight district/State removals workbooks by way of the producer's canonical page for this source, which transcribes them; confirmed the two cautions against the values by computing statewide any-suspension counts for every edition from the pinned all-editions artifact."
result = "Two editions carry a discipline-comparability caution and no others do. 2019-2020, sheet 'Important 2019-2020 Notes', against each of Violence/Vandalism/HIB/Substance Offenses, Police Notifications, HIB Investigations and Student Disciplinary Removals: 'Due to school closures in spring 2020, discipline data for the 2019-2020 school year may not be comparable to prior or future school years and NJDOE recommends caution in making comparisons.' 2020-2021, sheet 'Important 2020-2021 Notes', against the same four elements: 'Due to the COVID-19 pandemic, discipline data for the 2020-2021 school year may not be comparable to prior or future school years and NJDOE recommends caution in making comparisons.' Same sentence, different stated cause. Neither sheet is linked from the download page. 2021-22 and 2022-23 carry a notes sheet naming no discipline caveat; 2017-18 and 2018-19 carry no notes sheet of any kind. Statewide any-suspension counts across the eight editions are 50,284 / 55,971 / 39,646 / 4,644 / 52,135 / 61,132 / 59,877 / 56,564."
```

## Static rendering and omissions

The canonical profile build writes 587 district budget reports. Exactly 576
join the mart by zero-padded NJDOE district code and render the panel. These 11
exact-code nonmatches omit it instead of falling back to names:

| Code | District route |
| --- | --- |
| `0130` | `atlantic-highlands` |
| `0200` | `bass-river` |
| `0695` | `camden-co-ed-serv-comm` |
| `0810` | `chesilhurst` |
| `2120` | `henry-hudson-regional-monmouth-2120` |
| `2160` | `highlands` |
| `2305` | `hunterdon-county-educational-services-commission` |
| `4700` | `sea-isle` |
| `4720` | `seaside-park` |
| `5450` | `warren-co-special-service` |
| `5490` | `washington-burlington` |

South Orange-Maplewood renders 77 unique reported incidents and 1.1 incidents
per 100 students; 38 students receiving any suspension and a reported 0.55%;
13 students involved in an incident leading to police notification and a
reported 0.19%; and zero students on the arrest row as an exact reported zero.
All eight HIB-nature rows remain visible, including structural `n/a` rather
than zero for confirmed HIB under `No Identified Nature`. Every primary value
shows its exact workbook locator.

## Changelog

```toml ergo
[change]
date = "2026-08-01"
note = "Moved the pin to `nj-spr-discipline-safety-aq090-all-editions` and adopted its second artifact, the source-shaped census of all 37 tab-editions. Projects the eight-edition district removals record (65,701 rows, 2017-18 to 2024-25) plus the publisher's own coverage and identity registers. The 2024-25 artifact's SHA-256 moved with the release while no published value did, so the additivity proof is now a second, lineage-independent hash per table. The district profile gains a suspension history that breaks at 2019-20 and 2020-21 and prints NJDOE's two sentences where the reader meets those points, and states in words that no student-group trend and no arrest series is drawn."
issues = ["covid-editions-are-not-comparable", "an-unlegended-n-marker", "all-students-changes-row-shape", "one-edition-renames-the-removal-columns", "removals-sheet-lost-two-measures", "district-row-suppression-stops-in-2022-23", "student-groups-were-redefined-in-2024-25", "identity-evidence-starts-in-2020"]
```

```toml ergo
[change]
date = "2026-07-30"
note = "Widened the district projection from the three never-suppressed slices to every student group and every grade (49,950 to 620,292 consumer rows), carrying all 183,192 district-level suppressions into the warehouse for the first time; added a 54-row metric catalog; asserted the three pre-existing marts byte-identical; shipped the district-profile module and registered the ten issues a student-group discipline surface has to get right, including that `<5` bounds a headcount at zero rather than one."
issues = ["two-suppression-markers", "less-than-five-is-count-only", "less-than-five-includes-zero", "small-cell-rule-is-inconsistent", "sex-count-withheld-rate-published", "suppression-tracks-group-size", "published-zero-is-not-a-gap", "dimensions-effectively-never-published", "arrest-columns-are-named-police", "reported-incidents-measure-reporting"]
```
