Skip to content

Latest commit

 

History

153 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Homeroom

California's public school data, readable by the families it describes.

Working title. Not affiliated with the State of California or any school district.

Live at https://homeroom.chelseakr.com — every active California school, 10,534 of them, in English and Spanish. What is published there, and why the ask layer covers two of those pages rather than all 21,069, is under Status.

Quickstart

No acquired data and no network. site-offline renders every fixture school in both languages, which is the same renderer and the same code path the published site is built with — only the inputs differ.

uv sync --locked --extra ask                    # or: make sync
make site-offline                               # 3 fixture schools, English and Spanish
python3 -m http.server -d build/site-offline    # then open http://localhost:8000

The pages carry a fixture banner in both languages, because a page built from sample rows must never be mistakable for one about a real school.

Command What it does
make verify The single local gate; run it before opening a PR. Needs Node 22 for the markup and accessibility stages, which run npm ci. See CONTRIBUTING.md.
make site-offline The school pages, from committed fixtures.
make data-offline The same pipeline's JSON artifacts (data/out/), from the same fixtures.
make explain CDS=<code> One school's record from data/out/: every cell's state, its unit, and the CDE file and year it came from. A withheld or unreported cell carries no number — the value key is absent, not zero and not null. This is what a reporter cites.
make diff OLD=<dir> NEW=<dir> What changed between two publishes, by school and by cell state, over the artifacts rather than the 23,310-file markup diff. A cell that went from a number to withheld is its own event, never a value that changed to nothing; a source nobody supplied is one event, not thousands.
make dataset The artifacts as a citable dataset release under dist/dataset/: schools.csv where every measure is a state column and a value column, a Table Schema generated from the same header, one JSON record per school, and a manifest.json of SHA-256 digests — plus the whole directory as a byte-reproducible .tar.gz. Refuses a fixture build unless asked (ALLOW_FIXTURE=1).
make site The pages for a real school. Needs the CDE extracts in data/raw/, which are not in git — PROVENANCE.md names each file and how to acquire it.
make publish Rebuilds the committed site/ tree that is served at the live URL. Needs data/raw/ and ASK_ENDPOINT. Renders into build/publish-site/ and replaces site/ only once that tree is inside the limits its deploy is subject to.
make publish-limits Weighs site/ against those limits and prints what it weighs. make publish runs it over what it has just rendered.
make sources-check Reads CDE's HTML download pages and reports whether any of them now lists a file newer than the one PROVENANCE.md records as acquired. Fetches no data file. Not part of make verify.

The problem

California publishes an enormous amount of data about its public schools: enrollment, chronic absenteeism, teacher assignments, per-pupil spending, English learner progress. Almost none of it is legible to a parent deciding where to enroll a child, or trying to understand the school their child already attends. The files live in download pages built for researchers, the dashboard flattens everything into color bands, and commercial school raters compress it all into a single score with well-documented equity harms.

Homeroom joins the state's own data into plain-language, bilingual school pages, and holds one rule above all others:

It refuses to rank schools. No composite score, no letter grade, no ordering of one school above another. Each measure is shown on its own terms, beside the statewide and district context needed to read it, with its suppression and coverage stated. A number that cannot be shown honestly is not shown at all.

Honesty rules (ported from sibling projects, enforced in code)

  • A suppressed or masked measure (CDE masks small cells to protect students) renders as not published, never as zero, never interpolated.
  • "Not reported" and "reported as zero" are different facts and stay visually different.
  • District and statewide context comes from the state's own aggregate rows, never from adding schools together. A sum over a column containing masked cells is wrong, and a sum that skips them is wrong and looks clean, because it drops exactly the students the mask protects.
  • Every figure traces to a named public file with an access date (see PROVENANCE.md), and make sources-check says when CDE has published a newer one.
  • Coverage is a first-class output: how many schools publish each measure is itself published, so absence reads as absence rather than as a clean dataset.
  • English and Spanish from the first release. No account.
  • Google Analytics 4 counts page views (owner decision, 2026-09-17), added to the published pages after rendering by src/homeroom/analytics.py. It loads only on homeroom.chelseakr.com, never under Global Privacy Control, Do Not Track or the "Opt out of analytics" button on every page, with Google signals and ad personalization off; the landing page carries the full disclosure in both languages.

Data reality

Source files are downloaded from CDE's public data pages the way CDE intends: in a browser, by a person. The pipeline treats them as locally acquired inputs, with each file's origin, date, and name documented in PROVENANCE.md; drop them in data/raw/ and make data validates and builds from there. CI never fetches a source file, and a small committed fixture exercises every rendering case.

An acquisition date is only honest while it is the newest thing CDE publishes, so make sources-check reads each source's download page — the HTML index, never a data file — and compares the filename and posting note CDE lists now against the ones recorded in PROVENANCE.md's machine-readable source register when the file was acquired. It reports unchanged, newer, unreadable, or not checkable with the reason, and an unreadable page never reads as unchanged: a page that answers HTTP 200 with no file listing at all, which is what www3.cde.ca.gov returns to a non-browser client, is unreadable. .github/workflows/source-freshness.yml runs it weekly and keeps exactly one issue up to date; it acquires nothing, because acquiring a source is a browser step a person takes. That is narrower than the "CI never touches the network" this line carried until 2026-08-29: make verify-ci reaches a package index or an advisory database at uv sync, npm ci, pip-audit, npm audit, and the pinned uvx runs of semgrep and zizmor. What never crosses the network is the data. This mirrors the Afterward project's answer to the same provenance problem with federal endpoints.

The dataset release

The pages are for families. The dataset is for the readers who check them, and it carries the same rule: no blank means anything. Every measure in schools.csv is two columns —

column value
grades.GR_03__state reported, zero, suppressed or not_reported, never empty
grades.GR_03__value the number CDE published, filled exactly when the state is reported or zero

so a blank value cell cannot be read as a zero: the word beside it says which of the two absences it is. Those are the four cells the school pages render, mapped through one table, so the CSV cannot come to say something different from the markup. A published zero is zero, not reported, for the same reason the page labels it in words.

make dataset writes schools.csv, schools.schema.json (a Frictionless Table Schema generated in the same pass as the header, so a column cannot exist without a declaration), one schools/<cds>.json per school — byte-for-byte what make explain prints, from the same function — coverage.json verbatim, and manifest.json listing every file with its size and SHA-256. The whole directory is packed as homeroom-dataset-<date>.tar.gz, and two runs over one set of artifacts produce the same bytes and the same digest: members sorted, every mtime, uid and gid zeroed, and gzip's own timestamp zeroed too. A citable dataset whose digest moves is not citable.

The <date> is the latest access date in coverage.json — when the files were taken from CDE, which is the dataset's identity. Never a build clock, which would give two names to one dataset. A fixture build has no access date at all and is named homeroom-dataset-fixture; a made-up vintage on a file is worse than no vintage.

It refuses rather than publishes: a fixture build without ALLOW_FIXTURE=1, artifacts that do not state is_fixture as a boolean, a row count that is not exactly the profiles figure the build recorded (a truncated dataset presenting itself as a complete one), a school with no CDS code or a duplicated one, and schools that disagree about which measures they carry.

Citing it. Cite the tarball's file name and its SHA-256, both of which the manifest carries, together with the acquired file names, academic years and access dates it copies from coverage.json. A figure cited from this dataset should be quoted with its state: "23 students (reported)" and "withheld under CDE's small-cell rule (suppressed)" are different claims, and "no figure was published" (not_reported) is a third. Counts are what California published, not what is true of a school.

make dataset is not a CI job and cannot be one: data/raw/ is in .gitignore and never reaches a runner, so the real dataset is built on the machine that acquired the files. Uploading it to a release is a step the owner takes.

None of this touches site/. The dataset travels in a release, not on Pages, which is why it can be built while the published tree is at 86.8% of its 1 GB ceiling.

Status

The school-directory parser (the spine every other dataset joins against, via CDS codes) is built, tested against a fixture, and verified against the live file (acquired 2026-08-07): 18,396 directory rows parsed with no drift errors, yielding 10,534 active schools across 1,059 districts and all 58 counties, 1,238 of them charters. Those 1,059 districts carry only 1,048 distinct names: ten names cover two districts each, except "Jefferson Elementary", which covers three. Counting districts by name loses eleven of them, which is why the CDS code is the only key this project joins on. The 2025-26 Census Day enrollment file (269,090 rows, acquired the same day) parses end to end and joins that spine, and make data now assembles one profile per active school, with total, grade-span, and subgroup enrollment as three-status measures, and emits deterministic JSON artifacts: 10,534 profiles, byte-identical across re-runs, coverage published beside the data (9,860 school totals joined, the 698-plus-674 join gap counted in both directions, masks kept as nulls).

The first bilingual school pages are built (M4). One page per school per language, rendered from those profiles: identity, total enrollment, TK-12 grade spans, and 25 subgroup figures, each cell in exactly one of four states, with coverage in the next three columns. Birch Lane Elementary in Davis Joint Unified renders from the acquired files in English and Spanish, publishing 36 of its 40 figures (30 counts and 6 genuine zeros) and stating in words, for the other four, that the state published nothing. Every user-visible string exists in both languages: 228 keys per locale, zero present in one and missing from the other, enforced by test. The renderer's pages carry no script and no account, and reach nothing off this origin: since 2026-09-18 each school, county, district and landing page links a single stylesheet the same build writes (homeroom.css, at the root of the site, under that fixed name), and there is no font, no image, no CDN and no third party on any of them. The ask pages are the exception in the other direction — they keep their stylesheet inline, because ADR 0003 promises they fetch nothing at all until a question is submitted. The published pages carry one script, the same-origin, hash-pinned Google Analytics loader that homeroom.analytics adds after rendering, and nothing else.

Chronic absenteeism (D3) is the first measure Homeroom publishes that CDE masks at real scale, and it is now built end to end (M3). The 2024-25 file (341,490 rows, acquired 2026-08-21) parses, joins the spine, and renders on every school page: a total rate plus race/ethnicity, gender, and student-group breakdowns, each beside its district and statewide figure, read from CDE's own Charter School = All and DASS = All rows. Across the 10,534 active schools, the total rate is published for 9,718 of them, withheld for 83, and not published for 733 that the file never mentions; some subgroups are withheld far more often than that. The most-withheld is Non-binary (GX), withheld at 1,990 of the 2,045 schools that report it at all, 97.3%; the other 8,489 active schools carry no Non-binary row for CDE to withhold, so that share describes a much smaller slice of the state than the next one does. American Indian or Alaska Native (RI) is withheld at 9,350 of 9,801, 95.4%, and 9,801 is nearly every active school. Three categories are withheld for over 94% of the schools that report them at all. docs/SUPPRESSION-SHOWCASE.md carries the table and walks four real rows, one of each cell state, from the source file to the rendered markup. Grade-span categories are recognized so the file parses but are not rendered as a subgroup, the same choice already made for D2's age ranges.

What a cell can say, and how the four states stay apart on the page:

State On the page Never
Published figure the number, as published rounded, averaged, or derived
Published zero 0, plus the words reported as zero confused with an empty cell
Withheld (CDE's *) the words withheld to protect privacy, no digit shown as 0, estimated, or recovered from its siblings
Nothing published the words no figure published, no digit shown as 0 or left blank

Accessibility and translation are gated, not asserted. make verify builds the pages from committed fixtures and runs html-validate and axe-core (WCAG 2.0/2.1/2.2 A and AA, plus best-practice) over every page in both languages, and re-checks structure, EN/ES key parity, color contrast in both themes, and that every number in a data cell is a number the pipeline counted. Re-verified with M3's four new tables in the fixture build (2026-08-21): zero violations, same six rule sets. What none of that can do is look at the pages: layout, reflow at small widths, focus visibility in practice, and a screen-reader walkthrough in each language need a person, and that walkthrough has not happened yet. M3 deliberately kept its new tables at the same seven columns as the existing ones rather than adding more while that gate is open; see docs/RESPONSIBLE-TECH-AUDITS.md §E and RR-05 in docs/audits/residual-risk-register.md. What a person is meant to do, page type by page type and in both languages, is written down in docs/accessibility-walkthrough.md, whose results table is the record of it and is empty.

Teacher assignment monitoring (D5) is published on the school pages as of 2026-09-05, by the owner's decision recorded in docs/adr/0005-publish-teacher-assignment-monitoring.md. CDE publishes these files from the Commission on Teacher Credentialing's CalSAAS system: by school, how much teaching FTE sat on a clear credential appropriately matched to the assignment, and how much sat in one of the other authorization states the state tracks. The 2023-24 file (234,206,408 bytes, 1,528,796 rows, acquired 2026-08-21) turned out to disagree with the parser's provisional contract in every particular -- real column names, seven outcomes rather than five, FTE fractions rather than integer counts, and up to 150 rows per school rather than one -- and src/homeroom/assignments.py was rewritten to match what the file actually contains (PROVENANCE.md D5 has the full list). Each page now carries CDE's own whole-school row: the total teaching FTE, the seven outcome FTE counts, and the seven outcome shares, every one a copied cell, beside the district and statewide figures read from the file's own aggregate rows and with coverage stated in the next three columns. Nothing is summed and no share is divided out of a count, because a share Homeroom worked out would be a number about a real school that nobody published -- and on a masked row it would print a figure where the state withheld one. There is no headline "percent properly credentialed": one number standing for seven is the compression ADR 0002 refuses, and it is the most sortable thing the file contains. From 2026-08-21 to 2026-09-05 this paragraph said the opposite, because it was true then: make site was not given the file, homeroom.site accepted no argument for it, and whether to publish it was an open question tracked as issue #59. A build still given no D5 file publishes nothing and says so in words, which is tested. The remaining datasets (D4, the state dashboard indicators, and D6, per-pupil spending) were surveyed on 2026-09-05: both exist and are readable, D6 at a different address than this project had recorded, and neither is acquired or publishes anything. D4 turned out to be nine files rather than one, sharing 22 columns and not a schema, in which suppression is a blank cell and 0 means "No Color" rather than zero. D6 turned out not to be in the SACS files this repository had pointed at, which are district-level; the school-level source is CDE's ESSA per-pupil expenditure workbook, which publishes four components and no total, so the one number a reader would expect does not exist in the file and could only be computed. Both therefore wait on a decision rather than a download: whether an ordered Dashboard band may be shown at all under ADR 0002, and what "per-pupil spending" is allowed to mean when the state publishes no total. PROVENANCE.md D4 and D6 carry the measurements and docs/ROADMAP.md "M5 source survey" states the decisions.

Live at https://homeroom.chelseakr.com since 2026-08-22, by the owner's decision. Whether these pages belonged on the internet was a separate question about real schools and real children, and no build made it; a person did.

What is published there is every active school in the directory file: 10,534 schools, 21,069 pages, English and Spanish for each. Until 2026-09-05 it was one -- Birch Lane Elementary in Davis Joint Unified -- and this section said so. The ratio it described, one school out of the 10,534 the pipeline profiles, is no longer the shape of the site.

The ask layer did not widen with the pages. It is published for the schools named in ASK_SCHOOLS, which is still Birch Lane in both languages. Widening the pages was a decision about which schools to put in front of families. Widening the ask layer is a different decision, about an approved spend envelope: the service calls a paid model per question, a CloudWatch alarm watches daily invocations against that envelope, and it is the one surface here that can be asked something nobody reviewed. That decision has not been made. The two halves cannot drift apart quietly, because a school page carrying an ask link with nothing behind it fails test_no_published_link_points_at_a_page_that_was_not_published.

The landing page does not print any ratio; what it says, in both languages, is that Homeroom is in development, that the schools listed are the ones published so far, and that more are added as each source is acquired and checked. This paragraph said "the landing page says so" until 2026-08-29, which read as a claim that the 10,534 figure is on the page. It is not, and the page does not imply the state is covered either.

The site is rendered here and committed to site/, because it cannot be built in CI: the acquired CDE files never enter git and nothing in CI fetches them. The workflow in .github/workflows/pages.yml publishes that directory and builds nothing, so the bytes reviewed in a pull request are the bytes served, and tests/test_published_site.py gates them in make verify on a machine with no acquired file present.

Publishing every school put a number on that trade, and then the number started making decisions. site/ is 867.6 MB across 23,310 files, against the 1 GB GitHub Pages allows a published site — 86.8% of the cap, from 212 KB the same morning — and it packs to about 46 MB in git because the pages differ from each other in so few bytes. This paragraph said 836 MB across 21,076 files until 2026-09-05, which was the tree before the same day's 2,234 county and district pages went on top of it.

What the cap is now deciding, measured rather than estimated. Publishing D5 on the school pages, decided in ADR 0005 and not yet republished, is 8,723 bytes a page and so 184 MB more, which is past the ceiling on its own (#82). Ask pages for all 10,534 schools rather than two would add 303 MB and would not fit either. D4 and D6 will add measures to every one of the 23,310 pages. And the one large saving on the ask layer is not available: lifting the ask page's inline CSS and script into shared files is 10,993 of its 14,206 bytes, about 216 MB across the projected layer, and those bytes being inline is exactly what makes tools/ask-optin.mjs's zero-requests-on-load assertion true. So the scope of the ask layer is bounded twice over, by the spend envelope first and by this limit second, and make publish prints both counts when it finishes.

That argument is about the ask pages and does not reach the other four page kinds, which carry no script, made no request before this, and have nothing whose timing an inline stylesheet protects. Taking the stylesheet off those — issue #95, the renderer change landed 2026-09-07 — is 117.1 MB the next make publish will not write: 118,190,092 bytes of <style> across 23,305 files, less 5,341 bytes of homeroom.css and 44 bytes of <link> a page. Stated with its own arithmetic rather than as a rescue, because it does not rescue the budget: it takes the tree to 750.5 MB (75.0% of the cap), and a D5 republish on top of that is 934.3 MB — 93.4% of the ceiling, and over the 900 MB budget tests/test_published_limits.py holds. What it buys is the difference between a publish that GitHub would refuse outright at 105% and one that is inside the host's limit and over this project's own, which is a decision that can be made rather than an artifact that cannot be uploaded. The saving is counted here and not yet in site/: republishing is the owner's step, and until she takes it the served tree is the 867.6 MB tree above.

The owner's answer to the second bound is to move hosts rather than keep shaving bytes. deploy/site/ holds the shape that would replace GitHub Pages — a private S3 bucket behind CloudFront with Origin Access Control, published by .github/workflows/site-publish.yml over GitHub OIDC with no stored key — with what it costs at this size (about two cents a month) and what the new ceilings are (no total-size limit at all) both measured. Nothing there has been applied. No stack exists, no byte has been uploaded, homeroom.chelseakr.com still resolves to chelseakr.github.io, and .github/workflows/pages.yml is untouched and still the deploy families receive. The cutover and the rollback are the owner's to make, and deploy/site/README.md carries both in order, with what to check after each step.

That limit is written down for a build to read, in src/homeroom/publish_limits.py: the published tree against the Pages ceiling, sitemap.xml against the sitemap protocol's 50,000 URLs and 50 MB, and any single file against the 100 MB Pages will serve. Each fails at 90% of its limit rather than at it, so a tree that trips one is still a tree Pages will serve while it is sorted out — at 86.8% of the ceiling, the site has about 32 MB of that budget left, which is less than one new section on every page, as the D5 figure above shows. tests/test_published_limits.py holds the committed tree to those numbers, and to one more the sentinel sets: no published file may be larger than half of what tools/verify_live_site.py can fetch, because past that bound the daily check of the deployment stops comparing anything at all.

make publish reads the same numbers, one step earlier. It used to open with rm -rf site and then spend about a quarter of an hour rendering the replacement, which put the irreversible step first: a publish that could not deploy destroyed the working copy of a site that was being served, printed "commit it to deploy", and left the size to a later make verify — or to GitHub refusing the artifact while every check here stayed green. It now renders into build/publish-site/, weighs that, and moves it over site/ only if it passes. A refusal names the ceiling, the budget, the measurement and where the bytes are, points at the decision in #82, takes none of it, and leaves site/ exactly as it was.

tests/test_published_site.py also walks the published tree the way a family does, index to county to district to school in both languages, because a school that is published and unreachable is not published.

AI at the edges (ADR 0003)

As of 2026-08-21, by the owner's direction, Homeroom has an optional question-answering layer so a family can ask, in English or Spanish, what a school's page is saying: "Is chronic absenteeism a problem here?", "How many students are English learners?", "What does 'chronic absenteeism' mean and how is it measured?". The design, recorded in docs/adr/0003-grounded-ai-at-the-edges.md, keeps the founding rule intact:

  • It still refuses to rank. The service sees one school per request, so it cannot compare schools it cannot see; "is this a good school", "give it a grade", "which is better" get a fixed, reviewed refusal in both languages that the model does not write, and a second guard withholds any sentence the model did write that carries better/worse, grade, score, rank, or recommendation language. An adversarial evaluation suite targets zero.
  • The published dataset is the only evidence. Every claim cites a record (school CDS, measure, academic year) or a passage from a committed corpus of CDE's own definitions, and a verifier checks every number, every withheld cell, every comparison direction, and every quote against the data before anything is shown. A claim that cannot be verified is withheld and counted.
  • A withheld cell is never a zero, in prose any more than in a table.
  • The school pages do not change. They stay static and script-free and make no off-origin request. The opt-in is a link to a separate ask page for that school, which makes no request until a question is submitted, and a build not given a service endpoint renders neither.
  • Everything the model says is labeled AI-generated, unofficial, not a ranking, and not a recommendation. Spanish narration is labeled AI-translated and unreviewed.
  • It is deployed, as of 2026-08-22, by the owner's decision. The cost-bounded shape in deploy/ask/ is applied: one Lambda behind a Function URL in us-west-2, CORS and a server-side origin check locked to https://homeroom.chelseakr.com, reserved concurrency 2, a daily cap of 400 model calls per warm container, six requests per client per minute, and a CloudWatch alarm at 400 daily invocations to an SNS topic in the same stack. The cap is per container, not a shared ledger (deploy/ask/template.yaml DailyCap), so with reserved concurrency 2 the worst case is two containers each spending it; reserved concurrency is the bound that actually holds, and RR-09 carries this as an open item. The deployed model is Bedrock global.anthropic.claude-sonnet-4-6 -- the model the recorded evaluations name and the only Claude this account can invoke. make site still renders no ask page and no link until it is given an endpoint, so a build without one is byte-identical to a build from before ADR 0003. Rollback is in deploy/ask/README.md: delete the stack, rebuild the site without an endpoint.

Provider: the public anthropic SDK, default model claude-sonnet-5, configurable, credentials from the environment only. The evaluation harness and its five suites (ranking refusal, suppression fidelity, citation grounding, comparability, question structuring) live in evals/, and results are committed only from a recorded live run that names the model. The harness exits nonzero when a suite falls short of its target, and CI holds every committed results file to the same check, so a regression cannot be committed green (ADR 0004).

Recorded run (2026-08-22, evals/results/): 157 cases over real schools from the acquired files, on Amazon Bedrock global.anthropic.claude-sonnet-4-6 (the model this account could invoke; the code default, Sonnet 5, was not available to it, and the results say which model they are about). Ranking refusal 62/62, suppression fidelity 24/24, citation grounding 24/24, comparability 19/19, structuring 28/28. Across the run the verifier showed 511 sentences and withheld 23 before display: 11 quotes that were not verbatim CDE text, 5 numbers no cited cell publishes, 3 sentences carrying judgment language, 2 comparisons of the wrong shape, 2 definitions without a quote. Those 23 are the product working, not the model failing quietly: each one would otherwise have reached a family. What no suite measures is whether the Spanish reads well or whether a person, reading a sample of real answers, finds them honest; both are open (RR-07, RR-08).

Development disclosure

Built AI-assisted (Claude Code), with every claim, parser, and number verified against acquired source files and enforced by the test suite. The honesty rules above bind the tooling as much as the author: nothing ships that the data does not support.

Standards conformance

Governed by portfolio-standards (private).

Standard State
Responsible-Tech Framework Applies (see docs/RESPONSIBLE-TECH-AUDITS.md)
Code Quality Applies
Security & Supply-Chain Applies. make verify runs semgrep over this project's own source, tests/ included (semgrep's built-in ignore list drops tests/; the committed .semgrepignore replaces that list and does not), zizmor over the workflows under a hash-pin policy, pip-audit, npm audit, and a secret scan covering git history and the working tree, because history mode alone is blind to an uncommitted key. .semgrepignore excludes vendored, generated and built output, site/ among it, so the bytes actually served are gated by tests/test_published_site.py rather than by semgrep; this row said "the whole tree" until 2026-08-29. Every step in .github/workflows/ci.yml is accounted for by tests/test_ci_parity.py: each run: step calls a make target that make verify reaches, and each uses: step is either a setup or reporting action or a gating action registered against the target that reproduces it locally, which is what used to cover the secret-scan job, whose only step was an action. So the local gate is a strict superset of CI and the two cannot drift; the sentence said "every step ... is a make target", which was true only of the run: steps. CI runs make verify-ci and make secret-scan-history, which is all of it except the working-tree secret pass: in CI the working tree is the committed tree, so that pass has no uncommitted file to find. The secret-scan job was gitleaks/gitleaks-action until 2026-09-13, and this row's "secret scan covering git history" held only of make verify: the action scopes itself to the commits the triggering event names, which on the single-commit push that every squash merge into main is meant 1 of 138 commits, with fetch-depth: 0 set the whole time -- checkout depth decides what is on disk, not what gets read. The job runs the same make secret-scan-history walk now
CI/CD Applies
Observability Applies (Tier C, library/CLI; declared in docs/ROADMAP.md)
Accessibility Applies, in two halves, one of them open. Automated: html-validate and axe-core over every built page in both languages, plus structure, EN/ES parity and contrast checks in make verify. Review, not yet done: the keyboard-only and screen-reader walkthrough in each language, and the reflow check at 320 CSS pixels on the seven-column tables. A headless DOM cannot decide any of it. The procedure for that walkthrough and its record are docs/accessibility-walkthrough.md — all five page types, both languages, every cell reading UNMET with no date and no name, because nobody has walked one. Tracked as issue #6 and RR-05; owner Chelsea Kelly-Reif; held to its record by tests/test_accessibility_review.py. The pages are live, so this is open work rather than work waiting on a publication decision
Internationalization Applies (EN/ES is a launch requirement; the parity gate has been wired into make verify since ROADMAP M4 and fails the run, but main requires no status check, so CI reports it rather than blocking a merge on it — measured 2026-09-13, see RR-12)
AI Evaluation Applies as of ADR 0003 (2026-08-21): a prompt, a retrieval corpus, and a model-version surface now exist in src/homeroom/ask/. Five evaluation suites and their harness live in evals/; results carry provider, model, prompt version, commit, and date, and a test rejects results without them. See docs/RESPONSIBLE-TECH-AUDITS.md AI-EVAL and Governance
Documentation Applies
Quality & Metrics Applies (see docs/ROADMAP.md metrics ledger)
Performance Applies: the school pages are pre-rendered static HTML built from locally acquired files, with no client-side script and no network call at build time, and the pipeline is deterministic: re-running make data produces byte-identical artifacts. The optional ask service (ADR 0003) is a hosted route as of 2026-08-22; a measured answer takes about 5 s end to end against Bedrock claude-sonnet-4-6, and no latency objective is declared for it yet. The static pages declare none and need none: they are files. No page-weight or build-time budget is asserted in CI yet
AI Development Measurement Applies: this project is built AI-assisted and says so below. The outcome side is the metrics ledger in docs/ROADMAP.md, where every gate names its measurement and its AUTO/REVIEW disposition, and every day-one value was measured against a named acquired file rather than estimated. The diagnostic counters the standard names (sessions, tokens, share of generated code, acceptance rate) are not instrumented here, and by the standard's own rule they would be observe-only if they were: they never gate a merge
Incident Response Applies: SECURITY.md routes reports through GitHub private vulnerability reporting with a 72-hour acknowledgment target. The static site and the ask service are deployed as of 2026-08-22 and there is still no account; the ask service (ADR 0003) stores no question and keeps no user data, so the incidents this project can actually have are a wrong or mis-sourced figure on a school page and a model sentence that reached a reader unverified. The first is why masked cells are type-enforced to raise on read, why a number on a page that nothing counted fails the build, and why coverage is published beside the data; the second is why every AI claim passes a verifier and the withheld count is shown. A severity ladder and a committed postmortem template are not yet in the repository
Data Governance Applies: PROVENANCE.md is the register: every source is a named California Department of Education public file with its acquisition method, access date, and status, and a source that has not been acquired publishes nothing and says so in coverage.json. CDE small-cell masking is preserved as null, never zero and never interpolated; the CDS code is the only join key; no third-party or commercial data enters the pipeline. The artifacts are school-level public aggregates, not personal data, and the site has no account; page views are counted by Google Analytics 4 (owner decision, 2026-09-17; subprocessor record in docs/RESPONSIBLE-TECH-AUDITS.md), with GPC, Do Not Track and an on-page opt-out honored. Raw source files are never committed and CI never fetches them. The ask service (ADR 0003) sends a reader's question and one school's published records to the model provider for the duration of the request and stores neither; the subprocessor record is in docs/RESPONSIBLE-TECH-AUDITS.md under Privacy, owner-approved 2026-08-22
Release & Versioning Applies

License

Apache 2.0. Source data is California open data; per-source terms in PROVENANCE.md.

About

In development. California public school data, joined from CDE's published files and rendered as plain-language bilingual school pages for the families it describes, plus a question feature grounded in the same files. Refuses to rank schools: no composite score, no ordering. A suppressed measure renders as not published, never as zero. Unofficial.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages