firstbell

firstbell places a school’s morning absence calls through CALL-E, and refuses to close the ones that came back empty.

Both came back schema-valid. Only one of them found the child.

S-41010:35

Parent/guardian said Meera had fevers since last night and will probably be back tomorrow

parent_confirmed_aware
yes
reason_category
illness
expected_return
tomorrow
spoke_with
unknown
  • a two-bucket systemresolvedcase closed
  • firstbellresolvedThree fields the office can act on.
S-41051:00

Recipient was unaware of the absence and said Nithya P left for school this morning

parent_confirmed_aware
no
reason_category
unknown
expected_return
unknown
spoke_with
unknown
  • a two-bucket systemresolvedcase closed. Filed as reason_category unknown, parent_confirmed_aware no.
  • firstbellescalatedheld open and escalated to a safeguarding lead.
What these two calls are, and what the two-bucket row is not
S-4101 and S-4105 are two calls this software placed through CALL-E on 2026-09-04, to a consented line, on a scripted scenario with an invented pupil name. Both came back with a structured result the schema accepts. The fields are what CALL-E returned, verbatim, and the durations are the recordings’ own. The two-bucket row is a counterfactual and is labelled as one: it is the rule act 03 states, applied to these receipts, and not a measurement of another product.

One school morning, modelled

41 absences in one morning at a school of 600, at the national rate. Every tile is a family somebody has to telephone before the register closes, and from the desk they are identical. One of them is not a child who stayed home. Where the number comes from, and what happens when the morning is longer than the cut-off →

What these calls are, who consented, and how the two systems differ

The guardian was not aware of the absence. That is the answer that matters, and it is the only answer the call got. Why the pupil was away came back unknown. When to expect them back came back unknown. Those two are what the office needs before it can act, so a system that counts contacts would mark this “family contacted” and close it. This one files it escalated and puts it on a safeguarding lead’s desk, inside thirty minutes.

Two calls above, played from their own recordings, out of 20 this software placed through CALL-E; the money is computed over the 12 pooled in evidence/recorded-calls.json; act 02 holds both conversations in full. The recordings are held outside this repository. Every call went to the author’s own line, scripted and consented, and the pupil names are fictional. Act 01 says what the region ceiling costs, and act 07 is the run that shows it.

Technical verification and audit trail

Nine acts, every one of them folded. Open any of them for the receipts behind the two calls above: the money against CALL-E’s own billing, every rule broken on purpose with the test that caught it, the limits this entry does not clear, and the whole run on one command.

  • 12 real calls
  • 11 answered
  • 7 escalated to a person
  • $0.40 a call, mean of 19
  • The problem

    Answered is not the same as answered usefully.

    A call that connects and learns nothing looks exactly like a success.

    The three endings
  • What this does

    Three endings, not two.

    resolved closes the case. failed is retried. undetermined never closes: the call happened, it produced nothing usable, and the software says so instead of counting it.

    What the third one costs
  • What it changes

    The office gets a list, not a percentage.

    An attendance clerk opens a queue ordered worst first: safeguarding, then the children nobody reached, then the calls that learned nothing. Every row names the person it is waiting on.

    Run it yourself
  • Why it holds

    853 tests, and every rule broken on purpose.

    Every number on this page is computed from a recorded run rather than typed. Break one of the rules deliberately and a named test fails; the broken rules and what caught them are published.

    Every rule, broken

The two-minute path

Six destinations, in the order they answer the question a judge is actually asking. Everything else on this page is the evidence behind them.

  1. 01Watch a row fill in

    A real call, and what CALL-E returned for it: parent_confirmed_aware no, reason_category unknown. The fields fill in from the recording as it plays.

  2. 02One scenario, English and Tamil

    A different pair of calls from the one above: two conversations of different lengths, and the four fields underneath each of them.

  3. 03Three endings, not two

    What this software does with a call it could not get an answer to.

  4. 04What it costs, and against what

    For a buyer: the price above which a person is cheaper, beside what a district already pays for the system this sits next to.

  5. 05Run it, here, now

    The whole offline run is on the page: seven rows, three endings, and the totals. A button replays it line by line. No account, no key, nothing dialled.

  6. 06What a pilot would look like

    Two schools, six weeks, the four numbers measured before switch-on, and the children this software is not allowed to telephone. Leaves this page.

Why an unanswered message is not information, and what the run prints about this one
01

The school knew nothing, and had no way to find out.

An unanswered absence message is not information. It is an absence of information, and it looks identical whether the child is at home with a fever or never arrived anywhere.

The call above is the second kind. At 21 seconds the person who answered is asked to confirm they know the child is absent, and says they did not know, and that she left for school that morning. Nobody on the call could say where she actually was. The reason for the absence came back unknown, and parent_confirmed_aware came back no.

A school would have to answer these within 30 minutes (this project's default, which no district has agreed to).

Printed by the run itself, above the cases it refuses to close. The whole queue is in act 08.

Calls go out in whichever language CALL-E offers for the country. In the United States today that is English, and act 07 holds the refusal, printed by a run a reader can make on their own machine.
855
tests, 853 of them run here
358
rules broken on purpose to prove a test notices
12
real calls, counted in evidence/recorded-calls.json
none
CALL-E account needed to run the demo
02

Same call. Whichever language the family speaks.

One scenario, performed once in English and once in Tamil. The two conversations are not the same; what the platform returned for them is.

The missing-child scenario, in English beside Tamil

How one morning of unexplained absences is worked, from the register to the queue a person opensA four-stage diagram of one morning's absence follow-up, drawn as four cards on a single row with a safeguarding rail running above them. Stage one, a register of twenty-one unexplained absences, drawn as redacted rows. Stage two, calls placed in a wave and held at three lines at once, so a queue of waiting calls backs up behind the three that are in progress. Stage three, three endings rather than two: a reason given, which closes; a call that connected and learned nothing, which does not close; and no answer, which is retried. The middle ending is the only tinted row in the figure, because a system with only two buckets files it with the successes and reports full coverage for a child nobody heard about. A vertical bar between stage two and stage three carries every line to every ending, because which line placed a call says nothing about how that call ends. Stage four, a worklist ordered worst first, holding every case that did not close, each row repeating the glyph of the ending that put it there. Each of the four cards stands on its own plate, offset down and to the right, so the four stand at four heights on one board. Each carries a status tag on its header rule: twenty-one logged, three live, one open, four queued. The tag on stage three is the only lit one, because one open is the figure's argument. Running above all four stages on its own rail is the safeguarding axis, with dashed stubs onto the first three cards and one solid segment turning down into the head of the worklist. It sits above the stages rather than among them because a case can be escalated whatever its ending, and escalation sends it to the head of the queue rather than to a fourth bucket.Safeguarding, at any pointStage 01The registerno reason givenlogged21unexplainedStage 02Calls placedthree at once, rest wait3 liveLine 01Line 02Line 03Cap: 3 concurrentStage 03Three endingsone of them does not close1 openReason giventhe call closes hereNothing learnedconnected, still unexplainedNo answerretried, then queuedStage 04Worst firstthe queue a person opens4 queued1Safeguarding23rd no answer3No answer4Nothing learnedReason givenNothing learnedNo answerSafeguardingCall in progress
Twenty-one unexplained absences, calls held at three lines at once with the rest waiting, three endings rather than two, and the queue that is left. Safeguarding runs above all four and goes straight to the head of the queue.
EnglishS-41050:00 / 0:59
parent_confirmed_awarenoreason_categoryunknownexpected_returnunknownspoke_withunknown
  1. 0:00agentThis is an automated call from the school attendance office.
  2. 0:02recipientHello, who's this?
  3. 0:04agentI’m calling from the school attendance office.
  4. 0:06agentI’m calling about Nithya P, who was marked absent on September fourteenth,
  5. 0:10agenttwenty twenty-six; can you confirm you are aware of the absence?
  6. 0:21recipientWhat? No, I didn't know that. Well, actually, she left for school this morning.
  7. 0:22agentThanks.
  8. 0:23agentWhat was the reason for the absence?
  9. 0:31recipientUh, I, I don't know, I need to check on that.
  10. 0:32agentOkay.
  11. 0:32agentWhen should the school expect Nithya P back?
  12. 0:44recipientUh, after checking that, I'll let you know because I'm unaware that she is absent.
  13. 0:45agentOkay,
  14. 0:46agentI’ll note that you’re not aware of the absence. Anything you want to add in a brief note?
  15. 0:54recipientUh, no.
  16. 0:55agentNoted.
  17. 0:56agentThank you, bye.

CALL-E’s own noteRecipient was unaware of the absence and said Nithya P left for school this morning.

TamilS-41060:00 / 1:49
parent_confirmed_awarenoreason_categoryunknownexpected_returnunknownspoke_withunknown
  1. 0:00agentஇது பள்ளி வருகைப் பதிவு அலுவலகத்திலிருந்து வரும் தானியங்கி அழைப்பு.This is an automated call from the school attendance office.
  2. 0:04agentநீங்கள் ஒரு நபருடன் அல்ல, AI உதவியாளருடன் பேசுகிறீர்கள். எந்த நேரத்திலும் பணியாளர் ஒருவருடன் பேச வேண்டும் என்று கேட்டால்,You are speaking with an AI assistant, not a person. If at any time you ask to speak with a staff member,
  3. 0:13agentநான் மீண்டும் அழைப்பை ஏற்பாடு செய்வேன்.I will arrange a callback.
  4. 0:19recipientஹலோ. ஓகே.Hello. Okay.
  5. 0:21agentலக்ஷ்மி வி இரண்டாயிரத்து இருபத்தாறு செப்டம்பர் பதினான்காம் தேதி வரவில்லை என பதிவு செய்யப்பட்டுள்ளார்;Lakshmi V has been marked absent on September fourteenth, twenty twenty-six;
  6. 0:28agentநீங்கள் அவர் வராதது பற்றி அறிந்திருக்கிறீர்களா?Are you aware of her absence?
  7. 0:39recipientஇல்லையே என்னச்சு சொல்றீங்க? எனக்கு கரெக்ட் டா தெரியலையே. காலைல ஸ்கூல் க்கு தான் கிளம்பி போனா அவ.No, what are you saying happened? I don't know for sure. In the morning she left for school only, she did.
  8. 0:41agentசரி.Okay.
  9. 0:42agentஅது ஏன் என்று சொல்ல முடியுமா?Can you say why that is?
  10. 0:53recipientகரெக்டா தெரியல நான் செக் பண்ணனும். ஏன்னா அதை மோர்னிங் காலைல கிளம்பி தான் போனா ஸ்கூல் க்கு.Don't know for sure, I need to check. Because that morning in the morning she only left and went to school.
  11. 0:53agentநான் காத்திருக்கிறேன்.I am waiting.
  12. 1:00agentநான் காத்திருப்பேன்.I will wait.
  13. 1:05recipientபுரியல.Don't understand.
  14. 1:07agentஎந்த காரணத்தால் வரவில்லை என்று தெரியுமா?Do you know for what reason she did not come?
  15. 1:10agentஉதாரணமாக உடல்நலம், குடும்ப காரணம், அல்லது வேறு ஏதாவது?For example health, family reason, or anything else?
  16. 1:15recipientஇல்ல இல்ல அதான் இது.No no, that's what it is.
  17. 1:17agentசரி, நன்றி.Okay, thank you.
  18. 1:19agentஅவர் எப்போது மீண்டும் பள்ளிக்கு வருவார்?When will she come back to school?
  19. 1:21recipientஅதான் எனக்கு தெரியல. நான் செக் பண்ணனும். ஏன்னா அவன் மோர்னிங் ஸ்கூல் க்கு போனான்.That's what I don't know. I need to check. Because he went to school in the morning.
  20. 1:23agentசரி, புரிகிறது.Okay, understood.
  21. 1:25agentலக்ஷ்மி எப்போது திரும்ப வருவார்?When will Lakshmi come back?
  22. 1:37recipientஅது செக் பண்ணா தான் தெரியும். ஏன்னா மோர்னிங் ஸ்கூல் க்கு போனது மட்டும் தான் எனக்கு தெரியும். பட் இப்ப நீங்க வரலன்னு சொல்றீங்க.That will only be known if I check. Because that she went to school in the morning is all I know. But now you are saying she didn't come.
  23. 1:38agentசரி,Okay,
  24. 1:39agentநான் இதை மீண்டும் சரிபார்க்க சொல்லுகிறேன்; இன்று ஒரு பள்ளி பணியாளர் உங்களை பின்னர் அழைப்பார் நன்றி,I will tell them to check this again; a school staff member will call you later today thank you,
  25. 1:46agentவணக்கம்.Goodbye.

Each Tamil line is followed by its English. The Tamil is what CALL-E heard and returned. The English was written afterwards from that text, with a language model reading only the Tamil, and nobody spoke it on the call.

CALL-E’s own noteGuardian was not aware of the reported absence and said the student had left for school that morning; they need to check what happened.

60s and 110s of real recording, each playing whole at its own speed; the two scenes take about 20s and 34s, because the wait between turns is capped at 1.5 seconds

Different words, different lengths, 4 of 4 fields identical.

There is no Tamil-specific code in this app. Language is one column in the work file and one string in the request.

All sixteen comparisons, counted before the phone rang: 13 of 16 matched

Four scenarios, each performed twice, four enumerated fields per pair. The family was committed before any call was placed, so which comparisons get reported could not be chosen afterwards. All three mismatches are the speaker saying different things in the two calls, which a person recalling their own script from memory will do.

Each scenario performed once in English and once in Tamil, with whether the two calls agreed on every enumerated field.
scenarioen-INta-INagreed
illness, returns tomorrowS-4101S-41023/4
family emergency, no return dateS-4103S-41042/4
parent did not knowS-4105S-41064/4
refusalS-4107S-41084/4
03

The third ending is the one everyone gets wrong.

One call, and the only three states it can come back inOne call and the only three states it can come back in, drawn as three cards hanging off a single spine. Resolved is a ring closed round a solid core: the office has an answer it can act on and the case closes. Undetermined is the same ring half filled and is the only tinted card: the call happened and produced nothing usable, so a person has to pick it up and the software says so rather than closing it. Failed is the ring alone and dashed: nobody answered on any number, nothing happened and nothing is owed. A mark travels the spine into each ending in turn. The three glyphs are the same three used in the four-stage figure further down the page.One call, three endingsresolvedThe office has an answer it can act on. The case closes.undeterminedThe call happened and produced nothing usable.A person has to pick it up, and the software says so rather than closing it.failedNobody answered on any number. Nothing happened, and nothing is owed.
Every call this software places comes back as exactly one of these three. The third one is the whole argument: a system with two buckets has to file it with the successes.

One call. Three filings. Three different coverage numbers.

S-4105call S-4105, filed three waysparent_confirmed_awarenoreason_categoryunknownexpected_returnunknownspoke_withunknown
  1. wrongresolveda system with two buckets, closing anything that came back schema-validthe dashboard reports every family contacted, for a child nobody reached
  2. wrongfaileda system with two buckets, discarding anything short of a full answera call that did reach the parent is thrown away with the ones that never connected
  3. oursundeterminedthe call happened and produced no usable answerthe row stays open under a name, recovers no funding, and is never counted as coverage

One committed run. 7 calls placed, 7 answered, 3 closed with a usable reason. A call that connected and produced nothing usable is the difference, and it is not coverage.

7 of 7 contacted is what the same run reports with nowhere to put the middle four.

An automated dialler with no safeguarding triage does not close cases. It closes the only record that a case existed.

CALL-E filed a missing child as a routine transport absence. That call is S-3103. Its transcript travels with this page, with the school it names replaced by [school name redacted]; its recording is not played here. So did this software, until 2026-09-11.

This app made the first mistake itself.

The queue that comes out of it, named, with what each line costs

6

cases need a person. Nothing here is closed.

The language on 6 of these rows is read from the script of the call's own transcript, because this receipt predates the roster field that records it. 3 of the 6 were conducted in Tamil, so this queue needs a Tamil speaker on the rota rather than whoever is free. Each carries a 30-minute callback window and this receipt records no per-call end time, so the deadline is not shown. The field is written now, it was not on 4 September, and a countdown computed from the moment this page was built would be a deadline against nothing.

  1. S-4102

    The parent confirmed they knew, and the call never established when the child is coming back.

    Speak to this family first.

    safeguarding

    Tamil · 1 number tried

  2. S-4103

    The parent confirmed they knew, and the call never established when the child is coming back.

    Speak to this family first.

    safeguarding

    English · 1 number tried

  3. S-4105

    The parent did not confirm they already knew their child was absent. The call also ended without an answer the office can use.

    Speak to this family first.

    safeguarding

    English · 1 number tried

  4. S-4106

    The parent did not confirm they already knew their child was absent. The call also ended without an answer the office can use.

    Speak to this family first.

    safeguarding

    Tamil · 1 number tried

  5. S-4107

    The parent did not confirm they already knew their child was absent. The call also ended without an answer the office can use.

    Speak to this family first.

    safeguarding

    English · 1 number tried

  6. S-4108

    The parent did not confirm they already knew their child was absent. The call also ended without an answer the office can use.

    Speak to this family first.

    safeguarding

    Tamil · 1 number tried

Every row is a real call from 06-locale-matched-pairs.json, sorted by the same rule the program uses. Nothing here was arranged for the picture. One of them is the call this page opens on, and it plays up there; the recordings of the rest are held with the receipts, and the receipt file is on neither this page nor in the repository, for the reason evidence/README.md gives. The count holds without it: evidence/recorded-calls.json records that receipt’s 7 calls and the 6 of them that needed a human, which is the 6 rows here, and it carries the counts behind every money figure on this page.

This app made the first mistake itself. A parent refused to talk, CALL-E returned a schema-valid result with every required field set to "unknown", and the row was recorded as resolved. The receipt stays as it was written, uncorrected, because a corrected copy would record a run that never happened. What changed is the code, and a test now fails if the distinction collapses again.

Counts read from one recorded run of 7 calls, 06-locale-matched-pairs.json, a receipt held outside this repository and published nowhere, whose counts are committed in evidence/recorded-calls.json. 4 of 7 rows are still open and every one of them is named. The rate is resolved over attempted, so an open row can only ever pull it down.

04

Check us against CALL-E’s own billing.

CALL-E can match every row here against their own records, and they have no stake in these claims. The API returns one call identifier and the billing dashboard is keyed on another, so both are here, shortened at both ends, beside the structured answer each call brought back. A reader who is not CALL-E can check the count instead: it is committed in evidence/recorded-calls.json, and a note below says why the identifiers are cut.

$157,664

a year, for the system this would sit beside

$35.51

one student, one day, where funding follows attendance

$0.40

a call on the meter, averaged over the 19 CALL-E has billed since it repriced. The desk time one call removes is worth at most $0.35: a ceiling, not a saving

Where each of those figures comes from, and what would move it

$157,664 a yearOne named district’s student records bundle, on its own board record, for 11,353 students. $13.89 a student a year. One price on the record, not a market average. Chapel Hill-Carrboro City Schools Board of Education

$35.51 a student-dayTexas funds $6,215 per student in average daily attendance, over a 175-day year. Seven states funded on attendance as of 2022. Explaining an absence does not make a student present, so this run claims none of it. Texas Education Agency

$0.40 a callCALL-E publishes no price, and the one it charges moved while this page was being written. The left figure is the mean of the 19 rows it has billed this account since it moved the dashboard to credits: $0.06 to $1.36 a call, with 7 of them at the $0.06 floor. The 13 rows before that were $0.05 each, $0.65 over a month, and CALL-E’s own panel now labels them Legacy pricing. Nothing on this account was bought: the $8.20 it has spent came out of two grants worth $11.00, and the larger one was issued as 200 free calls, which is $0.05 each and buys 25 at the rate above. The right figure is the demo run: 4 of 8 attempts came off a desk at $23.55 an hour at three minutes each, less the safeguarding callbacks at $37.40. Not a saving: a ceiling. And not a measurement. That run answers against a test double, and its outcome mix is written down in firstbell/scenario.py: anybody can reproduce it with one command, and somebody chose it. The measured figure is below, over every call pooled in evidence/recorded-calls.json.

The most one call can save
$0.08
Where that becomes a loss
22.9 per 100 answered calls
What 11 answered calls cannot rule out
47 net-new per 100
The same bound if every escalation is a callback
86 per 100

The sample is every call pooled in evidence/recorded-calls.json, which is the sample this page was fixed on before any of it was measured, and these three rates are per answered call, because one of those calls reached nobody. The widest of them prices all 7 escalated calls as callbacks rather than only the ones this software says it created. That is 64 per 100 measured and 86 that this many calls cannot rule out, both above the 22.9 where the saving stops, so on that assumption this is a cost and not a saving. Quote the $0.08 above rather than the demo run’s $0.35: it is the smaller of the two, and it is measured over every call this page publishes rather than over a run. The demo run is the figure one command reproduces, and it is the higher of the two, so it is not the one to quote. Plan against the $0.70 a call this becomes if every escalated call is priced as a callback, which is the reading this entry holds itself to. The calls themselves were billed at a mean of $0.40 each over the 19 rows CALL-E has charged since it repriced, and at $0.05 each on the 13 before it, which its panel now calls legacy. Which of those two a district is living in is what the first week of a pilot measures, and nothing before that week can settle it. The paragraph below is the same figures with the objections they answer, and the demo run’s own numbers are in the two cards above.

$0.05 is one account’s billing on hackathon credit and not a price CALL-E stands behind, so a district confirms its own. Every one of those calls ran between 0m35s and 1m50s, all under two minutes, so they cannot tell a flat price from a per-minute one rounded up. This page’s receipt is a third run: 1 of 7 attempts removed, 2 of 7 answered calls became new work for the safeguarding lead, though 6 carry the safeguarding mark: net-new counts only a call that would have closed on its own, and a call already going to a person was going there anyway. 7 calls cannot rule out 66 per 100, against a crossover of 9.0 per 100: above that rate the callbacks this software creates cost a district more than the attempts it removes, which is the sentence a school board asks for and the one a vendor deck leaves out. At the far end of the bound the callbacks cost more than the calls save, and which end it is, is what a pilot measures in week one. Across all 12 calls this software has placed against the production API, the net ceiling is $0.08 and the crossover is 22.9 per 100, with 2 of 11 answered calls measured at 18.2. The safeguarding rule marked 7, and pricing every one of those as a callback rather than only the ones this software says it created turns that into a cost of $0.70 a call. That bound over-counts on purpose and it is the number to hold this entry to. python tools/money_across_runs.py prints every run’s figures from one piece of arithmetic. How many unanswered notifications a district handles in a morning is still a number a school office has and we do not, which is why every figure here is per call rather than per year. For scale only: the United States has 13,303 regular school districts and 99,297 public schools (National Center for Education Statistics). That is the size of the problem, not a claim about adoption.

What a pilot would look like: two schools, six weeks, and the four numbers measured before this software telephones anybody.

Every one of the twelve calls, with both identifiers, and what is held where
Every call placed, with the identifier the API returned, the identifier the billing dashboard is keyed on, and what CALL-E gave back.
call id (API, then dashboard)parent_confirmed_awarereason_categoryexpected_returnspoke_withoutcome
call_x_uA…Qjfw
15c7…1acc
yesillnesstomorrow·resolved
call_pDu9…7BpQ
6a76…d845
yesillnesstomorrow·resolved
call_FbGy…1f6A
f09a…52df
····failed
call_L9Ms…MYgA
b9ef…1fae
unknownunknownunknown·resolvedclosed on nothing
call_8cFQ…Mpag
49c1…4be0
yesillnesstomorrow·resolved
call_echb…NSYg
ec5c…26d5
yesillnessunknown·resolved
call_-vut…wCcQ
f50c…508e
yesfamily_emergencyunknown·resolved
call_4CDg…fH0g
392a…b406
yesotherlater_this_week·resolved
call_aOTj…QrLw
db78…f215
nounknownunknown·undetermined
call_-siz…AdLQ
13f2…c77a
nounknownunknown·undetermined
call_SjC0…RS2A
2043…4ba5
unknownunknownunknown·undetermined
call_Ijk9…Bnew
5f10…13e3
unknownunknownunknown·undetermined

6 of 6 committed receipts reached the production API and 12 of 12 calls carry the provider identifier. The rest had it recovered afterwards with a GET, which places no call.

The identifiers are shortened on purpose. A live call id is not a credential, because another account’s key cannot read our call, but it is an artifact of a real call to a real number and this repository asks contributors to keep those out of what they publish. CALL-E hold the billing records this table invites a check against and can match a row from what is shown; the unshortened list travels with the submission rather than on a public page.

What this repository holds, and what is held outside it.
transcripts, waveforms, the audio, and the unshortened identifiersnot in the repository. On this page in full, apart from the identifiers, which are shortened
the rules those calls produced, and the tests that hold themin the repository

The maintainer of this list requires committed real-call artifacts to be removed, and has said so even where the people on the call were team members playing a part on reserved numbers, which describes these calls exactly. So the recordings live here, the reasoning lives there, and tests/test_privacy.py fails the build if one crosses over.

05

Every rule, broken on purpose.

Every safety rule is verified against simulated failures.

How each rule is checked, and the defect it found

Each row is a change made to working code to check that a specific test notices. Every one was reverted and the suite returned to green. The number beside a row is how many tests failed while the change was in.

Number 18 found a live defect rather than confirming a rule. reached_production_api was computed from the configured base URL alone. A run whose every attempt died at the transport layer would still have published that it reached production.
0112642513134115+18
Tests that noticed each change. 1 of 358 changes were noticed by no test at all.
All 358 changes, and the tests that caught each one
The changes worth reading first, each made to working code on purpose, with the number of tests that noticed.
#the changetests that failed
2

Turn the consent check off, and the software phones a family that never agreed to be phoned.

if not item.consented: becomes if False:, disabling the consent gate

8
49

Turn the safeguarding rule off, and a parent who did not know their child was missing is filed as handled and nobody is told.

Make safeguarding_escalation return NONE for everything, so a parent who did not know their child was absent is filed automatically

13
53

Count the money from calls that were answered instead of calls that were closed, and the funding figure goes up every time a child cannot be accounted for.

Compute funding recovered from resolved rather than closed, so the money figure rises every time a child cannot be accounted for

2
57

Plant a real Indian mobile number where the privacy scan was not looking. This is the one row no test caught, and it is in the table for that reason.

Plant an assignable Indian mobile in plugins/firstbell-absence-calls/ with the privacy gate scoped to apps/python/firstbell, which is the scope it had when this was found and not the scope that ships. Row 58 is the shipped state

0
98

Remove the check on how a family can be reached, and a guardian who is deaf is phoned, reaches nobody, and is recorded as unreachable.

Remove the gate that refuses to dial a family recorded as unreachable by telephone, so a guardian who is deaf or hard of hearing is called, reaches nothing, and is filed under nobody answered

5
The other 353, in the same shape
The rest of the changes, in the same shape: what was altered, and how many tests noticed.
#the changetests that failed
1max_workers=self._concurrency becomes max_workers=None, removing the concurrency cap4
3Drop insufficient_balance from the double's API_ERROR_CODES4
4Match the production host by substring instead of hostname, so api.heycall-e.com.example.net passes2
5Write the transcript into every receipt instead of only when --include-transcript is given1
6Prefer the attempt's raw SIP code over the task's symbolic one, putting 603 in front of an administrator1
7Delete the SIP translation table, so a queue row reads the call failed with 6037
8Stop falling back to the task-level structured_result, which is the defect a real call exposed22
9Allow that fallback for fan-out too, which would file one family's answer against another family's child1
10Remove the check for a result whose required fields are all uninformative1
11Flag a result if any required field is unknown rather than all of them1
12Report placed when the response carries no usable created_at, instead of reporting unknown1
13Drop the clock-skew guard, so a service clock hours ahead reads as a freshly placed call1
14Read provider_call_id off the first attempt instead of the last, so a call that fell back to a second number cites the id of the attempt that did not connect1
15Change the test count stated in the README, which is what a reader runs and compares1
16Blank api_base_url on a receipt that still claims reached_production_api1
17Commit a receipt that no line of evidence/README.md describes1
18Compute reached_production_api from the configured base URL alone, so a run that exchanged no bytes with CALL-E still reports that it reached production1
19Credit every attempt to the machine instead of only the attempts behind records it closed, so calls that left a family unreached still count as staff time saved4
20Report a break-even of 0.00 rather than nothing when a run billed no attempts, turning could-not-compute into a free-looking number4
21Wrap the citation like prose, so the source URL breaks mid-path and the reader cannot open the thing the number came from3
22Drop source from the wage validation, so a staff cost can be asserted with no provenance at all1
23Shift one cited line number by one, the way any edit above it would1
24Keep a cited line number and change the symbol the sentence claims is on it1
25Point the ten-minute reading order at a file that does not exist1
26Delete the runtime anchors entirely, which a check written as "every anchor resolves" passes on an empty list1
27Put the extracted result back on the recipient when no per-recipient schema was asked for2
28Drop two task-level fields the production API returns on every response2
29Give an attempt our symbolic name for the outcome instead of the wire code the API sends7
30Commit an image rendered from the call recordings, which no text check can read1
31Store a vendor error message unchanged, when that message quotes the number it rejected2
32Leave the completion poll unprotected, so one failed read discards a call that was placed4
33Put one backspace byte inside a committed source file, which no editor or diff displays1
34Leave a real CALL-E error code in none of the three classification sets1
35Let one dispatcher instance run twice, inheriting the first run's cancellation1
36Accept two work items sharing an id, so one idempotency key covers both2
37Read a service clock an hour behind ours as a definite replay rather than unknown1
38Write the structured result to the receipt unmasked, which is how it shipped until this was fixed1
39Drop the comma from the separator class, so a number a vendor groups with commas is left whole3
40Let a calls.create timeout fall through to the catch-all, which is how it shipped until this was fixed: no retry, and a call that may have been placed recorded as FAILED2
41Cite in docs/images/README.md the line the caption used to name, which is how both stills shipped1
42Type a window bound back into the still generator instead of looking it up1
43Accept RetryPolicy(max_attempts=0), whose create loop never runs, so every item comes back FAILED with an empty reason about a run that dialled nobody1
44Stop escaping < in the JSON embedded in the page's <script> block, so a transcript turn containing </script> ends the data early and the rest of the page is parsed as markup1
45Delete the refusal that keeps a production key off every origin but https://api.heycall-e.com, so CALLE_BASE_URL can send the live bearer token to any host that is listening1
46Invert that refusal so it stops every destination, which would leave a tool that can never place a real call1
47Compare the base URL by hostname again instead of by origin, so http://api.heycall-e.com is filed in the receipt as an ordinary live call1
48Drop the scheme when normalising an origin, so a plaintext destination compares equal to the trusted HTTPS one1
50Ask the rule whether the answer is no rather than whether it is yes, which closes the missing and the unknown case as though either were a confirmation4
51Derive ItemResult.needs_a_human from the resolution alone again, so an escalated case never reaches the human queue6
52Stop sorting the queue, so a safeguarding case can sit below every ordinary callback and be reached last3
54Compute the resolution rate from resolved rather than closed, so the headline improves when the app finds something serious3
55Fail open when a caller's escalation rule raises, dropping the case the rule was written to catch on the one run where the rule was broken2
56Drop escalation= from the schema-valid branch, so the flag is computed correctly and then not carried7
58The same number with the gate scoped to every path this contribution adds1
59Point CONTRIBUTION_PATHS at a directory that no longer exists, which is what a rename looks like from inside the gate. Reported as 10 errors rather than 10 failures, because the guard sits in the helper every one of them calls10
60Have mask_id hand back the identifier it was given, so every call id on the page is whole again1
61Print the API id straight into the identifier table, shortening only the provider id beside it1
62Assign _call[_key] = _call[_key] in the loop that shortens the embedded call data, which is a masking loop that masks nothing1
63Have the README promise eight results on the page when the page carries eleven Needs the built page.1
64Change one unknown answer in the built page to yes, so four of the five countable non-yes answers remain Needs the built page.1
65Leave one whole provider id in the built page, in a comment the layout never shows, which is what a leak looks like when it is not in a visible column Needs the built page.1
66Teach another gate to skip and declare it nowhere, which is how a gate stops running without anybody deciding that it should1
67Drop a gate that still skips out of the register, so the suite goes quiet about one it already knew about1
68Declare a gate as unable to run when it runs perfectly well, because a register nobody prunes becomes a list of excuses1
69State a mutation count in the app README that the table does not have1
70State a different one in evidence/README.md, which is where this drift actually was: the index said forty-four while the table held sixty-eight1
71Round the published alert rate down from 45% to 35%, which is the direction somebody would round it if they were rounding on purpose Needs the built page.1
72Change one no answer on the built page to yes, so the page no longer supports the rate the README states beside it Needs the built page.1
73Put the wrong England absence figure back, which is the defect this gate was written after rather than a defect imagined for it1
74Add a plausible new percentage to the sourced section with no entry behind it, which is how the first one got in1
75Drop the source link and leave the figure bare, so it reads as sourced without being checkable1
76Plant a call id inside tests/test_privacy.py itself, where the module's own exemption used to hide it1
77State a kill count in README.md that the mutation table disagrees with1
78State a kill count in this file's prose that the row it names disagrees with1
79Say nine rows of the browser table record a state the page was really in, when seven carry the marker that says so1
80Take the marker off one browser row, so a failure the page really had reads as an invented one1
81Point a citation in call-e-feedback.md at the wrong lines of README.md1
82Shift a citation into another app so it names lines that exist and say something else1
83Reintroduce a stale gate count into evidence/README.md, the second file that states it1
84Switch a gate off with @pytest.mark.skipif rather than a call to pytest.skip1
85Switch a whole test file off from module scope with pytestmark1
86Make the masking loop assign each identifier to itself, checked against a page built from an authored fixture rather than the published one2
87Publish an answer in the register that the record behind it does not hold1
88Drop every field but the first from the register the page prints1
89Take the marker off one of the rows that needs a built page, so the note above the table counts more of them than the table carries1
90State a classifier test count in README.md that the module in the other language does not have, which is the count that was already wrong once1
91Put the credential guard back the way it was, refusing a key that starts with iams_live_ instead of requiring one that starts with iams_test_, so any key shape it does not recognise is sent to whatever host CALLE_BASE_URL names4
92Replace the retry backoff with return 0.0, so a failing upstream is retried at a family's number as fast as the network allows7
93Return the base delay for every attempt, so the policy retries at a fixed rate and never backs off2
94Remove the ceiling from the backoff, so attempt twelve waits half an hour and the policy becomes a way of never calling back2
95Have is_valid answer True for everything, which is what it did for every test in this suite before this row existed: it is exported in dispatch's __all__ and no code inside this app calls it1
96Have is_valid answer False for everything, so a schema-valid result is rejected1
97Drop the negation, so is_valid returns true exactly when the value is invalid1
99Keep the gate but stop the row claiming it needs a person, so the family is not dialled, is not a failure, and is also not on anyone's queue, which is the quietest of the three ways to lose them2
100Read an explicit voice=no as yes, so the column exists, is filled in correctly by the office, and changes nothing1
101Read a blank voice cell as no, so every family in a file that does not use the column stops being called10
102Ask the channel before consent, so a family that refused to be called is recorded as work owed to them on another channel1
103Stop excluding voice from context, which puts a disability record into the sentence the agent reads aloud on the call1
104Treat a voice value the reader does not recognise as reachable instead of raising, so a typo in that column silently decides that somebody gets telephoned1
105Put the skip counting back the way it was, skipped_no_consent=counts[Resolution.SKIPPED], so a run that was cancelled reports its unstarted rows as families who refused to be called2
106Stop counting the skips that are neither consent nor channel, so a reason added later disappears from the summary instead of showing up as unattributed2
107Put back a contrast pair naming a token page.css no longer declares, so the tool reports it unmeasured and the page keeps telling a reader that an unmeasured pair cannot read as a pass1
108Remove the pair covering a scoped re-cut of an ink token, so the colour a reader actually looks at inside a panel is measured by nothing1
109Put the locale back into lang="..." unescaped in the browser-side transcript renderer, the way it shipped, so a locale carrying a double quote closes the attribute and opens whatever follows it1
110Narrow esc to &, < and >, which leaves the check on the line above passing while the value it escapes can still close a quoted attribute1
111Rebuild the same attribute as a template literal with the value interpolated raw, which is how the hole survives a rewrite that touches nothing else1
112Retype the count under the browser-gate table rather than leave it to be counted, so the prose says eight rows record a state the page was really in where nine carry the marker1
113Drop the flattening from as_data and keep the length cap, so a roster field carrying a line break adds a paragraph to the instruction the agent reads on a call to a parent3
114Drop the length cap from as_data and keep the flattening, so one line is all a roster field needs and the line can be a brief1
115Return the roster value from as_data untouched, which is how build_task read it until this was fixed5
116Remove the sentence in build_task naming the three values as record fields, leaving a flattened injection to read as an aside the agent was told nothing about1
117Stop stepping the internal scroll boxes in the contrast gate, so the transcript's lower turns go back to being declined every run1
118Report the contrast census as zero instead of its real size, which is the number that makes an unmeasured count of zero mean anything1
119Take no census at all, so a run the probe never reached leaves no trace again1
120Identify a text run by its class and the first thirty characters of its text again, which folds two elements that share both into one and measures only one of them2
121Type the mutation count into the take-away card instead of counting the rows, which is the defect a reviewer found on the deployed page before this suite did2
122Put back The offline default never reaches it, the sentence that told a reader the default run does not use CALL-E when it executes three of the four anchored lines1
123Move calle-ai out of the runtime requirements, which would leave the README's reason the offline default runs CALL-E's own code true and beside the point1
124Type the size of the mutation table into the act 06 margin note again, the one of that page's three statements of it that the first version of this gate did not read1
125Say five real calls under a register that renders four, which is the shape a fifth receipt arriving would produce on its own1
126Change the enumerated field count in the locale fold so its own arithmetic stops reaching the comparison total it claims above1
127Stop sending X-Content-Type-Options, leaving the browser free to guess a type the deployment declared correctly1
128Open frame-ancestors from 'none' to 'self', which is the whole difference between a page that cannot be framed and one that can1
129Let style-src fall back to 'unsafe-inline', which grants every inline block and makes the three hashes beside it decorative1
130Grant script-src an origin the page never names, the shape a third-party allowance takes after the script it was added for is dropped1
131Change one character of the inline stylesheet's hash, which is what a policy describing an earlier build looks like1
132Narrow the header rule from every path to /index.html, so the modules and the eight audio clips are served with no policy at all5
133Hang a second inline event handler on the page, so 'unsafe-hashes' is widening the policy for something nobody decided to allow2
134Move the live ceiling from more than to at or more than, so a run of exactly the size the operator was told is allowed gets refused2
135Count rows rather than the calls a run would place, so a file of families who never consented is refused for spend it was never going to cost1
136Apply the live ceiling to the offline run, which phones nobody, capping a demonstration instead of a bill1
137Accept --max-calls and then ignore it, so the one deliberate way past the ceiling silently does nothing1
138Drop the number of families from the refusal, leaving an operator who is told the run was too big and not how big2
139Ask safeguarding_escalation only of rows whose resolution is resolved, so the four escalating cases in the run the page draws render as ordinary callbacks in the office queue1
140Let the report say the safeguarding window is this project's default whatever window was passed, so a district reads its own agreed clock into a number nobody chose1
141Accept --webhook-url and stop forwarding it to CALL-E, so a district's endpoint is promised an event that is never asked for1
142Let is_loopback answer yes to every address, so the double binds to a network on a flag nobody had to type1
143Parse --i-know-this-is-open and never consult it, leaving a guard that is written down and not run1
144Drop the override from the refusal text, so an operator with a real reason to open the port reads a wall instead of a choice1
145Put the animation data back behind a URL, so the policy refuses the fetch and the figure never plays1
146Hide the replaced still with element.hidden, which SVG does not implement, leaving it in the layout beside the animation1
147Size the animation to twice the still it replaces, so the page under it moves1
148Read a consent value nobody defined as a no, so a district whose export writes consented has every family dropped and reported as having refused1
149Read a row with fewer cells than its header, so a truncated export line becomes a family with no consent2
150Read a row with more cells than its header, so the surplus travels into the spoken instruction under the key None1
151Accept a column that appears twice, silently discarding one of the two values, which for phones or consent is the one that decides whether a family is called1
152Let a file that is not the promised encoding raise UnicodeDecodeError instead of naming the encoding it tried1
153Read a file containing a NUL byte, so a half-written export is treated as text and a child's name is spoken with a gap in it1
154Let a cell past the CSV field limit raise _csv.Error, which names neither the file nor the cause1
155Call from an export older than the window, which telephones the families of children who are in school today2
156Call from an export already called from, telephoning every family in it twice2
157Key the processed ledger on the filename rather than the content, so a job that rewrites the same rows under a new date stamp gets through1
158Record an export as called-from before its rows are known to be readable, which strands the operator who fixes it1
159Drop the sentence in the ledger header saying what deleting a line permits1
160Restore call["id"] on a create response that carries no id, so a request CALL-E accepted raises KeyError and is reported as a call nobody answered1
161Let a response shape the classifier does not model raise out of the handler after the id has left the in-flight set, so a placed, completed, billed call is reported FAILED with its id nowhere3
162Stop retrying a 5xx whose body is not JSON, which is the shape a proxy's own bad-gateway page has and the one class of failure a retry exists to absorb1
163Stop naming an unaccountable call in the run summary unless the run was cancelled, which is the branch that does not happen by itself1
164Stop printing the block that names a call this run placed and cannot account for1
165Drop not_recallable from --json, leaving a machine reader unable to see what the run left behind1
166Print the unaccountable call's id with no row beside it, so nobody can say which family it was for1
167Put back the n8n recipe's hardcoded escalation: "none" on the all-unknown branch, so the same call is a safeguarding case in Python and an ordinary callback in the workflow1
168Remove the schema check from the n8n recipe, so a value outside the enum closes a record there and does not here1
169Accept a nested object schema at construction, which problems() never recurses into, so every rule inside it is ignored and the answer is reported valid1
170Accept an array property, which has no items support, so a list of anything at all is reported valid1
171Accept an empty --again label, so a family is called a second time in one morning with no reason on the record1
172Stop normalising the label, so Locale Fix and locale-fix are two keys for one correction and the fix runs twice2
173Accept the label and never put it in the idempotency key, so the corrected run is refused exactly as the uncorrected one was1
174Stop printing the way out of a reused key, leaving a refusal that reads as a dead end until tomorrow1
175Stop recording the correction in the receipt1
176Stamp the double's created_at off one shared clock that advances 30 seconds a step and is never pulled back, so a run of 150 rows reports 27 of them with an unknown provenance1
177Ignore a pinned clock, so a test that asked for determinism stops getting it1
178Call phone.startswith("+") E.164 again in the double, so +91 5550 000001 is accepted and dialled offline where production would refuse it6
179Hardcode an off-palette accent in the README's path figure, so the drawing keeps a colour the page has moved on from1
180Drop the mono fallback chain from the path figure, so a reader without the webfont gets the browser default at a different width1
181Gut the path figure's desc, leaving a diagram a screen reader cannot report1
182Hand-edit the committed path figure, so the README shows something its generator does not write1
183Set an ending's second line in the palest ink the palette holds, on its own coloured plate, so a label on the page's own figure drops below WCAG AA1
184Put the escalation panel's dark ink on a mid-grey plate, which is the pairing a drawing loses track of because the ink and the ground are set in two different places in the file1
185Serve a document page with its markdown link left as written for the tree (receipt-provenance.md beside it), which is what all eight of them did on the deployment1
186Point an in-page anchor at an id that is not on the page, so the click scrolls nowhere and reports nothing1
187Give a published page an href in a scheme the link gate does not classify, to check it is counted unmeasured rather than quietly skipped1
188Make a gate raise instead of returning, by pointing a rail link at a deleted element, to check the run still reports the other fifteen1
189Stop a withdrawn consent record from refusing the call, which is the one misreading here that telephones a family who asked not to be telephoned4
190Make a record expiring today stop covering today, off by one at the boundary an expiry exists to define1
191Let one family's consent record cover any row that points at it, which turns a register with one good record into a permission for the roster1
192Ignore an unrecognised key on a consent record instead of refusing it, so a misspelled withdrawn_at reads as a record nobody withdrew1
193Count a row refused on a dated record by exact reason rather than by prefix, which files a family who withdrew consent under the bucket for a cancelled run1
194Resolve a header row matching two export formats by order instead of refusing it, so which column is the identifier is decided by the order this file lists formats in1
195Let a recognised export with no consent column dial anyway, which is the reading of a missing permission this software exists to refuse5
196Take only the first source column for a phone number, so a row carrying a number under sms alone has none2
197Hold a sibling behind a row nobody was going to dial, which reports a call saved that was never going to be placed3
198Drop the id fallback on an unparseable number, which collects every row whose number is punctuation into one household1
199Dial the last row of a household instead of the one the export put first, so a receipt cannot be read against the district's own file6
200Stop telling the dialled row who else in the house is absent, which asks a parent about one child while two more of hers are held1
201Count held rows in no bucket, which reports a household the run grouped as a run somebody cancelled1
202Stop checking who answered before reading what they said, so a brother saying the child is ill closes a record only a guardian can close4
203Read a call that did not record who answered as a call answered by somebody who is not the guardian, which invents a fact about a call while trying to be careful7
204Read everybody who answers as the guardian, which is the state this was written to leave7
205Report closures made with nobody recorded as zero, which hides the exposure by publishing a total that looks clean3
206Put the attempt count back under the word calls placed, six lines under a line saying six rows were attempted4
207Count a row this run dialled but made no attempt on as a call1
208Multiply an escalation bound that does not exist, which is what killed the whole page build on a run that answered nothing1
209Dial a row carrying a number the record does not name2
210Compare telephone numbers as text, so a register a person typed covers nothing2
211Read a comma-joined string as one telephone number1
212Count a refused row as a call that rested on a record naming no number1
213Stop printing how many dialled rows nobody can point at a number for2
214Record a refused row as exposure, which reports a family nobody rang1
215Build a forge URL for a cited file that is not in the tree2
216Ignore an unknown key in a scenario file1
217Load a scenario that binds nothing1
218Export a scenario without its transcripts2
219Accept a transcript turn that is not a speaker and a line1
220Take --outcomes and bind none of it1
221Report the exported scenario as current whatever is on disk1
222Default a scenario entry to nobody picking up1
223Compare a number with no digits in it as equal to another1
224Load a consent record whose phone entry is a word1
225Keep a work file phone entry that is a word and dial it1
226Shorten an identifier too short to have a middle1
227Load a scenario that answers on a negative index1
228Report every authorised call as one whose number was named1
229Label the two-file node --test command on the take-away card with the classifier file's own count, so the card offers 27 for a command that runs 35 Needs the built page.1
230Drop the cd from that command, so it names two files that are not under the directory this page puts a reader in and runs nothing at all Needs the built page.1
231Label double_conformance.py --check with a test count, which is what it carried before: that command runs no tests and prints one line Needs the built page.1
232State the node suite as 32 passing in the plugin's manifest.json1
233State the classifier's own count as 24 in that manifest1
234State the node suite as 32 passing in the plugin README1
235Say the receipts are on the evidence page in evidence/README.md, with a pronoun carrying the subject across the full stop, which is how the sentence was written1
236Say it in the README's money paragraph1
237Say it in the README's reading list1
238Say it in the README's privacy paragraph, eighty lines below the other one1
239Say it of the replay receipt in docs/receipt-provenance.md, which is the receipt carrying the claim that no telephone rang1
240Say it in the provenance line make_shape_fixtures.py stamps inside every generated fixture2
241Say it in that file's own docstring1
242Say it in the comment over the generated response fixtures in tests/test_dispatch.py1
243Say it in the footer under the escalation queue, with the page named first and the receipts after it, which is the wording the first version of the gate walked past Needs the built page.2
244Say it in the caption under act 03, which is the page telling a reader that a file is on the page Needs the built page.1
245Print one more queue row in the footer than the page drew Needs the built page.2
246Take the row the telephone cannot reach out of the district export, returning the channel gate to being demonstrated by nothing2
247Rename the channel column in that export, so every row reads as reachable2
248State the collected test count flat on the stat card, where two of them skip Needs the built page.2
249Point the transcript provenance at a document that is in no clone of this repository Needs the built page.2
250Have the transcript provenance call the waveforms the only audio that ships, on a build that ships the recordings and plays them Needs the built page.2
251State four runtime call sites above a list of five2
252Drop the directory from act 08, so neither of the two commands it prints runs from the root of a fresh clone Needs the built page.2
253Take the language ceiling off the page, which is what a reword of act 01 does to it now that it is no longer welded to the masthead Needs the built page.1
254Point the language ceiling at an act this page does not have, so the one limit a school trustee can check names no demonstration Needs the built page.1
255Put the language promise back in the masthead, now that the limit answering it has moved one screen down Needs the built page.1
256Declare --receipts required again in tools/throughput.py, so a reviewer running the documented command is told they used the tool wrongly1
257Report the missing recordings from tools/video_facts.py as a SystemExit, which exits 1 and reads as a check that failed1
258Take the shared first word off the page builder's own answer, so four tools report the third outcome in one word and the fifth in its own1
259Strip the flag, the environment variable and the committed file out of one COULD-NOT-MEASURE message, leaving a reviewer told what is missing and nowhere to go for it1
260Say in the README that the OneRoster run prints the platform refusing Spanish on a real call, when no call has been placed in Spanish and the refusal is the double's1
261Say the same in the caption under act 01, in the words the reviewer who asked for that caption wrote it in Needs the built page.1
262State the escalation count the receipts recorded before the correction as the current one, in the file that narrates the correction two paragraphs below it1
263Offer the calls placed as the sample in all three rows of a table whose column is a rate per answered call1
264Call the sample twelve calls in the sentence a district is told to take from that table, where the figures are over the eleven that answered1
265Label the bound on the money card with the calls placed rather than the calls answered, eleven lines above a paragraph that reads it correctly out of the same record Needs the built page.1
266Label the bound on the money card with the sample it was measured on and not the quantity it bounds, so 24 per 100 reads as what eleven calls cannot rule out about escalations, when the escalation rate they measured is about twice it Needs the built page.1
267Drop the escalation rate's own bound from the money card while the sentence under it still counts three rates, leaving the wider of the two readings unpublished Needs the built page.2
268State the wrong number of rates in the sentence generated beside them, so the card's own summary of itself is off by one Needs the built page.1
269Put the escalation bound in the column headed net-new per 100 answered calls, which is the mistake this document records making once with 24 against 50.4: two right figures and a comparison that is not one1
270Drift the cost the pessimistic reading prices out to by a cent in the money document, away from what the tool computes1
271Compute the escalation rate's bound off the net-new count, so the two bounds are the same number and the card publishes the narrow reading twice while calling one of them the widest Needs the built page.3
272Drift the derived absence rate by one per thousand in the line that shows the arithmetic, so the document publishes a rate its own registered inputs do not produce1
273Round the headline volume up to a number the derivation does not reach, which is the figure a reader carries away and the only one most will remember1
274State the wrong rate for a 175-day school year, hiding how much the largest lever on this figure actually moves it1
275Remove the publisher's definition of unexcused, which is the sentence that makes the volume figure a ceiling on what this software would call about rather than a measure of it1
276Publish the annual saving and drop the annual cost the widest reading of the same calls produces, which is the vendor move this entry was built to argue against1
277Close a record by reading a reassuring phrase out of the transcript, which is the improvement somebody would plausibly make to this tool and is the exact defect the rest of this entry exists to catch1
278Decide the outcome here instead of calling the function the live path calls, so a district adopting this over its own dialler gets a second rule that agrees with the first only until one of them changes1
279Silently ignore a result field the schema does not define, so a district that mistypes one field in its mapping gets records filed undetermined for a reason nothing on the output names1
280Abort instead of reporting could-not-measure when there is nothing to read, folding a third outcome back into a failure1
281Audit another dialler's calls without passing the numbers they were placed to, so a consent record covering a different number than the one dialled passes. Consent attaches to the number called1
282Judge a call nobody answered on the result attached to it, and treat a record that does not say whether it was answered as though nobody was there1
283Escape the limits list in act 07 with the escaper that escapes everything, so the two commands that section tells a reader to run reach the page as literal backticks instead of code Needs the built page.1
284Write the markup into the limits data as well, which is the shape act 07 really shipped: the command a reader is told to run arrives with its code tags visible as text around it, and an apostrophe arrives as a numeric character reference Needs the built page.4
285Promote the backticks in a mutation row and stop, which is what the page did for as long as the table has existed: twenty-six rows show the marker under this table with its asterisks Needs the built page.1
286Promote bold and stop, so the one row drawing a distinction between any required field and all of them shows the emphasis as punctuation instead Needs the built page.1
287Label the ceiling on the money card as what a call can save, eighty lines from a README that calls the same figure a ceiling and not a saving Needs the built page.1
288Drop the sentence naming which of the four money figures to quote and which to plan against, leaving a reader to pick Needs the built page.1
289Mark a drop directory's export as one calls have been placed from at the moment it is read, which is before the confirmation flag, before the key check, before the credential origin, before the call ceiling, and on every offline run2
290Mark the export whatever the run did, so a rehearsal against the bundled double, which rings no house, burns the file the morning's real calls come from2
291Mark the export on a wave that dialled nobody, where every row was held for a person on consent, a shared household number or a language this cannot speak, and those rows are tomorrow's work out of the same file1
292Record that the run stopped without setting the flag every worker reads before it dials again, so an operator's Ctrl-C is written on the receipt and the wave carries on out of the queue and out of the backoffs3
293Exit zero from a run that was stopped part way, which is what a cron job wrapping this reads as a morning where every family was called1
294Count the spend ceiling on consent and reachability alone, which is two of the four things that stop a row being dialled, so a file placing two calls is refused as four families and the refusal names the wrong number to raise it to1
295Read a record's answered as yes on any word, so a call nobody picked up is closed on the result sitting beside it, which is this entry's founding defect arriving through the adoption path3
296Iterate a single telephone number written as a string, so a consent refusal tells an attendance officer the record covers one number and the row carries twelve1
297Accept a CSV naming one column twice, where the reader keeps the last value silently, so column order decides whether a child's absence is closed1
298Crash on a records file that is not UTF-8, taking the exit code this tool reserves for a consent finding, which a district exporting from a Windows codepage hits on the first accented surname1
299Crash on a consent register that does not validate, so a script cannot tell one of these calls resting on nothing from a typo in the register file1
300Exit zero from --fail-on-uncovered on a call resting on a bare boolean, while the line above the exit reads on a boolean and the flag's help says exit one when a call rests on no consent record1
301Print the closed rows inside the queue, under a heading about what needs a person, each carrying the sentence that nothing here needs one1
302Publish the absence volume and the escalation rate and never multiply them, which is what a district buyer said was worth more than any figure already on the page1
303Understate the escalations a morning's volume produces, so a pilot is sized on the calls rather than on the callbacks and the queue it creates is blamed on the software1
304State the commit-provenance disclosure as an exact pair, which both halves of change on every push and which nothing recounts, making the entry's one honesty disclosure the one claim it does not check1
305Read the pair on the page's first screen out of a sentence in the README instead of the measurement sitting beside it, so a stale sentence and a stale card agree with each other and disagree with the suite1
306Name records in a fixture's own comment that the fixture does not contain, so a reader following the comment finds neither1
307Print the other N where N is all of them, so a sentence says the rule marked five calls and the other five gave nothing usable1
308Annualise the ceiling without subtracting what the calls cost, against a loss on the same line that is already stated net, which runs in this entry's favour on the one page a board reads1
309Read the cancel flag before the first attempt only, so an item whose first attempt timed out waits out its backoff and telephones the family after the operator has pressed Ctrl-C, holding a request that may already have started that same telephone ringing2
310Take the pair on the page's first screen off the recorded pass count rather than the tests the tree can run, which is a number the gate holding the file against the page lowers by failing, so a stale card has no value it can correct itself to1
311Read a placed call's id out of the poll response rather than out of the create that returned it, so a body answering a poll without one produces a queue row carrying no id beside a receipt naming the call, and the row a clerk reads says nobody was telephoned1
312Put one of two records for the same pupil in the closed pile while the other says a person has to ring back, so the pile a clerk stops reading at holds a child whose absence nobody has explained1
313Answer a spreadsheet that exported its header and no rows with a sentence about blank and commented lines, which a spreadsheet has neither of, leaving the district the right exit code and no idea what to fix1
314Set the count in the sentence under the money card next to the row that makes it true, so the two cannot disagree, and the gate written to catch them disagreeing cannot fail1
315State twenty-eight unexplained absences per 1,000 as the figure to use rather than a ceiling on it, which is the sentence that stops a reader multiplying the per-call money by every absence a district records1
316Call a string method on the path argparse hands back, so a records file with nothing in it raises rather than printing could-not-measure, which turns the one outcome this tool reserves for what it cannot judge into a traceback1
317Blank the English on one Tamil turn in the gloss file, so a line a reader is promised a translation of gets an empty one, which renders as the Tamil alone with a gap under it1
318Leave a Tamil word inside an English gloss, so the translation a reader was given to read is partly the thing they could not read1
319Add a gloss row for a line that holds no Tamil, so the file that exists to say which lines were translated starts claiming a line nobody needed translated1
320Empty the gloss file's statement of what wrote the English, so a reader deciding how much to trust a translation has nothing to decide from1
321Stop the builder refusing a Tamil line the gloss file has no English for, so the page publishes one untranslated line in the middle of a scroller and nothing says so2
322Stop the builder refusing a call in another language that the gloss file does not name, so a call added later reaches the page with none of its turns translated1
323Look each gloss up by its position in the list rather than by what the line says, so a transcript the transcriber split differently puts every English sentence under the wrong Tamil one, with a gloss present on every line1
324Decide which turns need English from the call's locale rather than from each line's own text, so a call labelled ta-IN that opens with an English announcement demands a translation of English, and the authored fixture the suite builds a page from stops building at all1
325Drop the English span from a rendered turn while leaving it in the published data, so the transcript a reader with no JavaScript meets is the one version of it with no translation2
326Drop the note under a translated transcript saying the English was written afterwards, so a page presenting a translation stops saying it is one, which is a claim about what was on the recording1
327Retype one consonant of the Tamil quoted in docs/the-legal-surface.md, which is the defect that was actually in the tree: the fourth word of the opening announcement carried the wrong one of two similar letters, inside the paragraph whose whole argument is that the disclosure a parent hears is the disclosure on the recording1
328Remove the English under a Tamil line in a document, so one file in the submitted tree prints two sentences of Tamil with no translation anywhere near them, which is where this started1
329Delete the attrs row from the runtime table, which is the defect that was actually in the tree: a direct requirement of the CALL-E SDK with no licence recorded, under a sentence saying nothing else was needed at runtime1
330State a version for httpx that is not the version installed, so the file describes a dependency tree that a reader checking it would not find1
331Call certifi MIT. It is MPL-2.0, and it is the only weak-copyleft row in the runtime table, so this is the mistake an author makes by assuming a licence rather than reading one1
332Say the browser-gate lock file resolves forty packages when it resolves eighty-three, so a count nobody could check sits next to a claim that all of them are permissive1
333Let one browser-gate dependency arrive under GPL while the file still says none is copyleft, which is how a licence claim goes stale: not by being written wrongly, but by a lock file moving under it1
334Remove the note explaining the AGPL dependency while requirements-dev.txt still pins it, so a reviewer of an MIT repository meets an AGPL import with nothing written about it anywhere1
335Put a kill count of 1 back beside the substring row in docs/proving-a-gate-fires.md, which is the defect that was actually in the tree: the document a reader is told to reproduce the mutations from copied a row whole and disagreed with the table it copied from, and the gate that checks these counts read only the README1
336State a kill count in the README that the table does not measure for the substring row. The sentence used to name a test instead of a number, which is why nothing checked it while its three neighbours were checked1
337State a call count on the first-screen dateline that evidence/recorded-calls.json does not measure, so the one number a reviewer meets before the fold is the one nothing checks Needs the built page.1
338State a price on the dateline that the account was never billed, against the figure read off the usage panel on 2026-09-07 Needs the built page.1
339Write the dateline's four numbers into the builder as literals instead of computing them. This is the failure the gate exists for: four values that are right the day they are typed and silently stop being right1
340Render the dateline when no receipts are on the machine, so the first screen carries four numbers no reader of that build can check1
341Guard the sibling sentence on the raw roster value while rendering the sanitised one. This is the defect that was in the tree: str.strip() leaves a zero-width space where as_data removes it, so a name made only of format characters passed the guard and the instruction that would have reached a real call asked why nobody was absent1
342Take the sanitiser off the sibling names, so a format character in a roster cell reaches the words CALL-E speaks2
343Use the singular verb however many siblings there are, so a call about two children asks why they is absent1
344Drop the sibling sentence entirely, so a guardian with three absent children is asked about one and the other two records are held on an answer nobody gave3
345Match committed transcript text by the key name transcript_turns instead of by the shape of a turn. This is the defect that was in the tree, and it is the worst kind: tools/glosses.json holds 53 turns of four real calls under turns, so the one committed file carrying real conversation was the single file this gate could not see, and the suite was green because the gate was looking in the wrong place1
346Count a turn only when it carries both text and speaker, so the Tamil and English pairs the gloss file is made of stop reading as dialogue1
347Empty the declared exception list while the file it names is still tracked, so real conversation sits in the tree with nothing recording that anybody decided it could2
348Stop the gate walking into nested objects, so a conversation one level below the root of a committed document passes1
349Render the documents with commonmark alone, which has no tables, so six documents publish their table rows as paragraphs of pipe characters. This is the defect that was in the tree: what-a-pilot-would-look-like opened its Shape section with eleven rows of | Scale | Two schools in one district. | set as running prose, on the page the money card sends a reader to3
350Skip the table pass entirely, so no cell carries the name of its column and an empty header row stays on the page as a rule above a band of nothing2
351Treat a column with no name in the header as an ordinary value rather than the term the row is about, so the money document's first column loses the mark that stacks it first and sets it in the label face2
352Take the header row off every table rather than only off one whose cells are all empty, so a table that names its columns loses the names2
353Return to what this module did for as long as it existed: keep the set of allowed tags as a comment and check nothing against it, so any tag the renderer emits reaches a public page with no style written for it1
354Take one document off the published list while it is still cited by name from another page, which is how docs/district-ingest.md and docs/consent-record.md came to be unreachable1
355Accept a name in the withheld register with no reason written beside it, so a document can be kept off the site by an empty string1
356Leave a document table with only the roles a stylesheet leaves it, so the stacked form under 38rem is a run of blocks to a screen reader: no row, no column, and no header attached to any cell2
357Take scope off the header cells of the page's five tables, so a header stops saying whether it runs down a column or across a row at the width where the stylesheet has already taken the implicit answer away Needs the built page.1
358Take the caption off one table, so it is announced by its size and nothing else Needs the built page.1

A test that has never been observed to fail has not been shown to test anything.

06

Two things worth taking, whatever you are building.

01

Record the identifier the vendor is keyed on

The id an API returns and the id its billing page shows are not always the same one. Recording only the first makes a receipt uncheckable by anyone outside the repository that wrote it.

02

Make the safety rules prove themselves

Deterministic safety triage guarantees no unconfirmed call slips through: a call that came back without a confirmed answer is owned by a person, not closed by a model.

How that is checked

358 deliberate changes, each reverted, each recorded with the number of tests that failed. Row 57 is the one no test caught, and finding out why is what this table is for.

07

What is not true.

Five limits, each with what would close it, reported here without complaint.

Five limits, and what would close each one
  • CALL-E offers English and no other language in the United States. Every figure priced on this page is American, and the language column cannot be delivered to a United States number today. Every call this project has placed went to an Indian number, where Tamil and Hindi are available.

    Nothing in this app changes it. Run python -m firstbell --work-file examples/absences-oneroster.csv and the second row prints the refusal in the platform's own words. Until it changes, the part of this that works in a United States district is the three outcomes, the consent gate and the structured reason, over whichever dialler the district already owns. That is a command and not a consolation: python tools/adopt_call_records.py --records examples/other-dialler-records.jsonl --consent-register examples/other-dialler-consent.json files six calls this software never placed, says why each landed where it did, and audits all six against the district's own consent register. One of them carries a transcript in which a parent says plainly that the child is at home with her, and it still goes to a person, because reading a guardian’s confirmation out of prose is the defect this entry was built to catch wearing better clothes. It also narrows the ceiling above: it binds while CALL-E places the call, and a district with Spanish-speaking families can keep the multilingual dialler it already pays for and take this layer over what that dialler returns. Nothing in that tool reads the language of a call, because it never reads the call.

  • Calls to India arrived from a United States caller identity, shown as Oakland, California. A family will not answer an unknown foreign number about their child.

    Fixable by the operator with a local number on the account. Nothing in this app changes it.

  • CALL-E has no cancel endpoint. Once a call is placed there is no route to stop it.

    The concurrency cap is the only brake that exists, so it is set low and the run reports what was already in flight when a stop was requested.

  • The agent does not enforce its own exit. On one call it announced it was ending, then restarted its opening disclosure.

    Reported to the vendor. The transcript is committed.

  • The locale result rests on one bilingual speaker who knew what was being tested, one language pair and one region.

    It is a pilot, and it is written up as one. The pre-registered comparison count is committed so nobody has to take the framing on trust.

08

Run the whole thing with no account.

Test our verified system offline right now, with no API key and no account.

  1. cd apps/python/firstbell
  2. pip install -r requirements-dev.txt
  3. python -m firstbell --work-file examples/absences.csv
Watch that exact command run, line by line

The local double is mounted as an httpx transport underneath a real calle.CalleClient.

Produced by running exactly that when this page was built, and it is all below. Watch it run to see the rows land one at a time.

 the real output of the command above, verbatim
OFFLINE. No telephone call will be placed and no CALL-E account is needed.
The CALL-E SDK is running: this is a real calle.CalleClient with the local double
mounted on its transport, so every request and every error is CALL-E's own code.
7 row(s) from examples\absences.csv, concurrency 3.

  [ok   ] S-1041       schema-valid answer received
  [ok   ] S-1042       schema-valid answer received
  [ok   ] S-1043       schema-valid answer received
  [HUMAN] S-1044       the call completed but returned no structured result
  [skip ] S-1045       no recorded consent to be called
  [HUMAN] S-1046       nobody answered after trying 2 number(s)
  [SAFEG] S-1047       schema-valid answer received, escalated as safeguarding and not closed automatically

What this run was worth
  attempted            6
  resolved             4   schema-valid reason on record
  of those, escalated  1   answer received, still not closed
  undetermined         1   call happened, no usable answer, needs a person
  failed               1   nobody reached on any number
  skipped, no consent  1
  attempts placed      8   on 6 call(s): a row with two numbers can take two
                       (no telephone call was placed)
  resolution rate      50%   closed, not merely answered

  consent
    on a record        0   dated, voice, attendance, not withdrawn
    on a boolean       6   a column that says yes, which is not a record
                           docs/consent-record.md is the schema that replaces it
                           this run used none. For the path with records:
                           python -m firstbell --work-file examples/absences-with-consent.csv \
                             --consent-records examples/consent-register.json

  who answered, on the records this run closed
    a guardian         3   the call recorded a parent or guardian on the line

  reached in-language
    en-IN              1
    hi-IN              1
    ta-IN              1
  non-English families 2 of 3 resolved

  still open, by language
    en-IN              1   needs a person
    ta-IN              2   needs a person

  funding recovered    not claimed
                       Explaining an absence does not make a student
                       present, so no attendance funding is recovered by
                       this call. Seven US states funded on daily
                       attendance as of 2022 (PPIC, citing the Urban
                       Institute). Pass --funding-rate with a source
                       if your jurisdiction is one of them.

  staff time avoided
    attempts billed     8
    attempts removed    4   behind the 3 record(s) this run closed
    attempts still open 4   on somebody's desk, so not counted as saved
    break-even          $0.20 per call, for every minute one manual attempt takes
                        so cheaper than the desk below $0.59 a call at 3 minutes an attempt
                        $23.55/hour, from $48,980 over 2,080 h. Secretaries and administrative
                        assistants, Educational services; state, local, and private, 2025.
                        Source: US Bureau of Labor Statistics, Occupational Outlook Handbook
                        https://www.bls.gov/ooh/office-and-administrative-support/secretaries-and-administrative-assistants.htm

  work this run adds
    net-new escalations 1 of 5 answered call(s) would have closed
                        without the safeguarding rule, so they are work
                        that did not exist before this run
    added               $0.08 per billed attempt, for every minute one
                        callback takes the safeguarding lead
    ceiling after it    $0.35 a call, at 3 minutes for each of the two
                        (from $0.59: the line above ignores this)
                        4 attempt(s) removed at 3 minutes is $4.71 of desk time,
                        less 1 callback(s) at 3 minutes, $1.87 of counsellor time,
                        over the 8 attempt(s) billed
    saving ends at      50.4 net-new per 100 answered calls. Above that,
                        the callbacks this rule creates cost more than the
                        attempts the run removes. This run measured 20.0.
                        $37.40/hour, from $77,800 over 2,080 h. School and career counselors and
                        advisors, Elementary and secondary schools; local, 2025. Source: US
                        Bureau of Labor Statistics, Occupational Outlook Handbook
                        https://www.bls.gov/ooh/community-and-social-service/school-and-career-counselors.htm

3 case(s) need a person. Nothing here is closed:
  1 of those cases is safeguarding: the parent did not confirm they already knew.
  A school would have to answer these within 30 minutes (this project's default, which no district has agreed to).
  !! S-1047       schema-valid answer received, escalated as safeguarding and not closed automatically
     S-1044       the call completed but returned no structured result
     S-1046       nobody answered after trying 2 number(s)

Source, tests and receipts: a pull request into CALLE-AI/awesome-phone-call-agents, under apps/python/firstbell. The branch was not pushed when this page was built, so the pull request is in the submission form rather than linked here.

A demo film runs 2 min 44 and is measured in evidence/film.json, which records its length, its shot count, how much of it carries sound and the digest of the file. It was not uploaded when this page was built, so its link is in the submission form too.

Where to go next

Everything this rests on, and how to leave this page to check it.

Two pieces of this you can take

  • The offline CALL-E

    A CALL-E written from the published API, mounted on the SDK's own transport, so the client under test is the shipped one. Every offline run in this entry is measured against it, and the record proving it matches production compares 11 recorded responses path by path and type by type.

    cd apps/python/firstbell && python tools/double_conformance.py --check 38 API paths, 0 missing

  • The same rule as an n8n recipe

    The three-outcome classifier is not locked inside a Python CLI. It ships as an importable n8n workflow, generated from the tested module by a committed script so the two cannot drift. It imports inactive, and its dry run places no calls and needs no API key.

    cd plugins/firstbell-absence-calls && node --test examples/classify.test.mjs examples/workflow-shape.test.mjs 35 tests, no n8n installed

Every document, and every outside figure with its publisher
  • What a pilot would look like

    Two schools, six weeks, the four numbers measured before switch-on, and the children this software is not allowed to call.

  • The money, in full

    Why the headline figure is a ceiling and not a saving, what the safeguarding rule costs at the grade it lands on, and the rate above which this software costs a district money.

  • The file your district already exports

    OneRoster, Clever or a header nobody here has seen, read without editing it. The two columns no export carries, what filling them costs, and why three children in one house are one telephone call.

  • The legal surface

    Seven questions a district's counsel asks before this software telephones a parent. FERPA, the TCPA, retention, and which four of them are still open.

  • The consent record

    A column that says yes is not a record of permission. The shape of one, the eight checks a call passes before a telephone rings, and the three decisions that stay with the district.

  • Locale is not only a hint

    Why the same call in Tamil is one column in a work file and not a second code path, and where that stops being true.

  • Proving a gate fires

    How each check in this repository was broken on purpose to confirm it notices, and what that costs when it is skipped.

  • Where a receipt comes from

    What is in a receipt, what was added afterwards, and how to tell a real call from a replay without trusting this page.

  • The offline CALL-E, and how to take it

    A CALL-E that dials nobody, written from the published API and checked against 11 recorded production responses. Two ways to mount it, and the three things about it that are not true.

The outside figures, and who published them

  • 14 million United States students chronically absent in the 2021-22 school year, which the publisher defines as missing at least 10 percent of school days

    Institute of Education Sciences, read at source 2026-09-07

  • 17.63% persistent absence rate across the autumn and spring terms of 2024/25 in England

    Department for Education, read at source 2026-09-06

  • 19.23% the same rate a year earlier, autumn and spring terms 2023/24

    Department for Education, read at source 2026-09-06

  • 10.53% the pre-pandemic rate, autumn and spring terms 2018/19

    Department for Education, read at source 2026-09-06

  • 78.3% share of the United States population aged 5 and over who spoke only English at home, 2018-2022 American Community Survey five-year estimates

    US Census Bureau, read at source 2026-09-05

  • 21.7% the remainder, who speak a language other than English at home

    US Census Bureau, read at source 2026-09-05

  • 99,297 Operating public elementary and secondary schools in the United States, school year 2023-24

    National Center for Education Statistics, Common Core of Data, read at source 2026-09-07

  • 13,303 Operating regular school districts in the United States, school year 2023-24

    National Center for Education Statistics, Common Core of Data, read at source 2026-09-07

  • 157,664 Total purchase cost approved by one North Carolina school district for its annual PowerSchool contract renewal, 2024-25, paid from the capital outlay technology budget

    Chapel Hill-Carrboro City Schools Board of Education, Agenda Abstract, 15 August 2024, read at source 2026-09-07

  • 11,353 students enrolled in Chapel Hill-Carrboro City Schools, North Carolina, in the 2024-25 school year, which is the year of the system renewal priced above it

    National Center for Education Statistics, Common Core of Data, US Department of Education, read at source 2026-09-07

  • 6,215 Texas Basic Allotment per student in average daily attendance, school year 2025-26

    Texas Education Agency, read at source 2026-09-07

  • 48,980 US dollars, median annual wage for secretaries and administrative assistants in educational services, which is the grade of the work this app takes off a school office desk

    US Bureau of Labor Statistics, read at source 2026-09-07

  • 77,800 US dollars, median annual wage for school and career counselors and advisors in local elementary and secondary schools, which is the grade the work this app creates lands on

    US Bureau of Labor Statistics, read at source 2026-09-07

  • 5,958,444 California students in the 2023-24 eligible cumulative enrolment, which the publisher defines as non-exempt students expected to attend more than 30 days. This is the denominator the absence-volume figure in docs/what-a-pilot-would-look-like.md is divided by

    California Department of Education, read at source 2026-09-08

  • 5,481,732 California students who missed one or more days of school between 1 July 2023 and 30 June 2024. The publisher calculates Total Days Absent from this count and not from enrolment, so the derivation has to as well

    California Department of Education, read at source 2026-09-08

  • 13.1 average days absent in California in 2023-24, which the publisher defines as Total Days Absent divided by the count of students with one or more absences

    California Department of Education, read at source 2026-09-08

  • 41.4% the share of all California absence days in 2023-24 that were unexcused, which the publisher defines as days a student was absent without a valid excuse, excluding out-of-school suspension

    California Department of Education, read at source 2026-09-08

  • 180 days of instruction a California school district is penalised for offering less than, used as the length of the school year the per-day figure divides by

    California Legislature, read at source 2026-09-08