Suzanne Sallaj

Build log

The build log: the month of Curb, day by day

On the evening of August 7th I parked at a bus stop with the engine running, and the app I had been building for a week had a small breakdown. Eleven times it decided I had parked. Ten times it changed its mind. The correct warning arrived five minutes later, as I pulled away.

That was day three. This is the whole month, in the order it happened, from the running log I kept the entire time. So it's what the notes say, not what I remember.

Part one · On paper

A week of specs before any code: what the agent may decide, what it hands back, how it speaks. Most of it survived. The detection plan did not.

Part two · On the street

Five drives. Motion-based detection lied in both directions, and the unplug moment replaced it. The spec caught a real failure in my own build.

Part three · In the data

Ten million citations become rules, and every shortcut that would have let the engine fool itself gets found and closed.

About fifteen minutes in full. Each part opens with its summary, then folds. Open what you want to see.

Part one

Part one: the specs, before any code

Before there was an app, or an engine, or a phone, there were six documents about how something should behave.

Read this part in full
Jul 25 to 30

The specs came first

Six documents in five days, and not one of them has a screen in it. What kind of relationship this thing was proposing to have with a person. Which of its jobs it could be trusted to do alone. What it must never do under any setting. How it should talk when it isn't sure, which turned out to be most of the time.

I decided the agent's character before I knew whether any of it was buildable. Written down like that it sounds backwards. It is the single decision I would repeat without changing, because nearly every hard call in the next thirty days had already been answered in one of those files, and the calls that hadn't were the ones that took a whole day.

One line from the behavioral spec did more work than the rest of the month combined: doubt resolves toward speaking, not toward silence. A false alarm costs a few seconds. A missed ticket costs money and a tow. Those are not the same price, so the agent has to be more sure a spot is safe before it stays quiet than it has to be that a spot is risky before it speaks up. I had no idea how many times I would end up pointing at that sentence.

Confidence against consequence severity, with the boundary between speaking and staying quiet leaning as the stakes rise.
That sentence, drawn. The bar to stay quiet climbs as the consequence gets worse, so the same confidence that earns silence on a time limit gets you a warning on a tow zone.
Aug 3 to 5

An engine with nowhere to put it

The rules came before the app that would use them. By the fifth there were 61,829 scored parking rules in a database, worked out from just under nine million tickets the city had written, and no iPhone project anywhere.

The idea is simple and slightly ridiculous. Los Angeles has never published a complete list of where you can and can't park. It does publish every ticket it issues, with the violation and roughly where. A ticket is the city saying, on the record, that a rule existed at that spot at that time. Enough of them and the rule comes back into focus. I tested it on one month of data before committing to anything and it produced 148 blockface rules for a single neighborhood with confidence scores that spread from 0.39 to 1.00, which is exactly what you'd want. Some things it knows. Some things it's guessing at.

Alongside the rules: 85,101 street segments, 60,372 fire hydrants, 34,943 metered spaces, 864 sweeping routes, 155 permit districts. The city, reassembled from the pieces it publishes separately and never joins up itself.

Four tiers of evidence: ground truth read off a pole, the city's own records, inference from ten million tickets, and corroboration from hydrants and transit stops.
What the engine is allowed to rest a claim on, strongest first. The hydrants and bus stops are the odd ones out, and that is exactly why they are useful.

Part two

Part two: five field drives

Five days, five drives, and the approach I had spent two weeks designing did not survive the fourth one.

Read this part in full
Aug 6

Xcode to a phone, in a day

The log records this whole arc as a single line, and it deserves to stay one: Xcode project, then the simulator, then my nineteen mockups arriving, then a home screen built to them and running on real data, then the whole thing on my iPhone. Roughly twenty commits by the end of the night.

The app's first act on arrival was to count its own rules twice. Once with a plain count, once by stepping through every row, because a partially copied database will report a perfectly plausible total and then fail halfway through a read. 61,829 rules, 85,101 segments, 3,568 meter limits, 51,494,912 bytes, matching the count taken off the source file before Xcode existed. It's a small thing. It's also the first time the project checked itself against itself and I remember being relieved that the numbers matched.

Aug 6 to 7

The first two drives

The app met a street and broke in five ways no desk test had found. The motion detection flickered. Location fixes went stale. There was no heading at the moment of parking, which matters because heading is how you know which curb you are against. The detector announced a park while I was still driving. And the home banner never left demo mode. All five fixed the same day, with tests that replay the actual field log so the bug can never quietly come back.

One thing worked first time. The first curb the app ever guessed: heading 199.7 degrees, southbound on Las Palmas, west side. Correct. A sample of one, and I wrote it down anyway because it was the first time the whole idea had touched a real street and not been wrong.

Then I parked on a red curb and the app told me to pay the meter.

That one turned into the day's real work. Rules lived at the level of a blockface, a whole side of a whole block, and a ten meter stretch of red paint is invisible at that grain. So I took 1.5 million red curb citations, projected each one along its street, split them by side, and clustered them. Out came 28,619 red zones on 12,221 segments with a median length of about ten meters, which is the length of actual paint rather than the length of a block. To check they were real I looked at where they landed: 72% sit at a fire hydrant, against 33% for the same zones shuffled to random positions. And my own park that afternoon, at 55.7 meters along the block, fell inside the detected zone at 25 to 60. The mistake became the test case.

Aug 7

The third drive, and the phone that fell asleep

This was the drive that answered every open question about background detection, all in the bad direction, and none of it was visible until I added breadcrumbs to the log.

The app showed Parked at every red light. The real park was created sixteen minutes late, at the moment I picked the phone back up, and it was placed 700 meters from the car, at my feet where I was standing on a block with completely unrelated rules. It was never announced. Nothing was scheduled. The two hour timer I had specifically driven there to watch start did not exist.

What the drive settled, permanently: the background timer does not fire. Motion sensing delivers nothing once the screen locks. The one stream that keeps arriving is location, and it is the only reason the app stayed alive at all. Any logic that could only move forward on a tick or a motion event was dead the moment the phone went in my pocket.

Seven fixes, each with a regression test replayed from the log. The test suite went from 37 checks to 48, and I verified the ten new ones fail against the old detector, reproducing the drive exactly: zero parks created, zero announced, position at infinity.

Aug 7, afternoon

Fourteen decisions before dinner

Between the third drive and the fourth I sat down with the scenario matrix and ruled on every situation nobody had decided yet. Fourteen of them. Seven became working code and tests before the end of the day, which is where the 42-check policy suite comes from.

There's a caution in the log from that session that I've kept ever since, because it was already the second time it had bitten. The suggestion column in the matrix was the agent's, not mine, and suggestions were never to be promoted into decisions just because they'd been sitting in the sheet long enough to look settled. Where I overruled a suggestion, the overrule governs.

Aug 7, evening

The bus stop

I parked at a DASH stop with the engine running. The phone's motion sensor flickered between automotive and stationary every second or two, and each flicker killed the park it was in the middle of settling. Eleven detections. Ten cancellations. The correct warning, "Do not park here, red curb, $93", finally arrived five minutes late, as I was pulling away.

Earlier in the same drive, creeping 400 meters toward a red light in traffic had read as the phone walking away from a parked car, and the app pushed a loading zone warning mid-drive while the motion sensor was saying automotive the entire time. Two failures, opposite directions, same source.

I fixed both that night. But I also wrote down, for the first time, that I might be fixing the wrong thing.

Aug 8

The pivot

Four drives had shown the same thing from four angles. Inferring the park from sensors was not a part of the system with bugs in it. It was the source of the noise, and every silent failure I had traced back to the same place.

So I stopped inferring. Plug in to drive, unplug where you park.

It asks something of the person, which is the sort of thing you are trained not to do. I took the trade because of what it buys. The plug bounds the app's attention: plugged and driving is the only state where it watches at all. Never plugged is not a failure, it is the app being deliberately asleep. Which means that when Curb is quiet, the quiet means one thing instead of three. The entire project is about not letting silence be ambiguous, and it had taken me four drives to notice that the detection layer was the biggest source of ambiguous silence in the system.

The build split into its own line that morning, tagged at the moment of the decision, and never merged back.

Five checks that must all hold before the agent speaks, starting with the plug.
After the pivot, the plug became the first of five gates. Each one fails differently, and four of the five failures are silences the agent understands.
Aug 9 to 10

Drive five

The first weekend on the new contract. The findings document opens by saying the detection layer produced zero complaints, and that everything left was in the data or the decisions. Which is where the pivot had said the problem would go.

So the problems were more interesting. At 737 N Vista on a Sunday afternoon, the app said Good Standing, move by Monday 10am. The Monday sweeping was right. But the sign also said two hour parking, and no parking after midnight, and neither of those made it into what I was told, so the thing I actually needed to do first was invisible.

The soonest-binding obligation must lead. My note in the findings, and the rule the ranking now follows.

Same block, different lesson. The app thought the whole segment was metered. The meters were three physical spaces at the corner, 95 meters from where I'd parked, responsible for 210 of the 216 metered tickets on that street. The data had flattened three corner meters into "the segment is metered", the same disease the red curb work had just cured, and the inventory carried per-space coordinates the whole time.

Part three

Part three: the tickets become rules

The tickets had been trying to tell me things. This is the week I started listening, and found a whole city missing.

Read this part in full
Aug 10 to 13

What the tickets were actually saying

Fines first. I had been carrying them as lookups and they were measurable, so I computed what the city actually charged. Peak hour had been sitting at $68 from a single sample and is really $163. Bus zones are $293, the most expensive mistake in the dataset.

Then what I'd been treating as one bus restriction split cleanly into two on timing alone. "Exclusive for buses" puts 52% of its tickets in the 4 to 6pm hours and about 1% at weekends, which is a rush hour lane. "Parking in bus zone" is flat across all seven days, which is a permanent stop. Merging them, which I had been doing, turned a weekday peak into a round the clock ban.

Then a real error, not a refinement. I'd been reading a restriction's hours as one span from the first ticket of the day to the last. That collapses "no parking 7 to 9 and 4 to 6" into "no parking 7am to 6pm" and tells someone a curb is off limits through a middle of the day when it's completely legal. Measured on Vermont, 41% of peak hour blocks have two windows. The fix was to look for the gaps in the hourly distribution instead of the edges, and the bimodal blocks that had scored badly under the old method scored well under the new one, because the shape had been wrong rather than the data.

Hourly peak hour tickets on a Vermont block: 28 at 7am, then 403, 287 and 177 at 4, 5 and 6pm, with the single 7am to 7pm span the old method drew across the gap and the shipped 4 to 7pm rule.
One block of Vermont. The tickets cluster at 7am and again from 4 to 6pm. The first version of the engine drew one span from first ticket to last and called eight hours of legal parking restricted. The shipped rule sits where the evening tickets are.

Then the one that could have cost somebody a tow. Enforcement sweeps through at the start of a window and leaves. On a curb posted four to seven, the tickets run 34 at four, four at five, one at six. So the inferred end lands at six for a rule that runs to seven. Harmless for what the app tells you to do, because it never states a return time. Dangerous for the question "am I exposed right now", which was declaring a tow zone clear while a car sat in it. Erring late costs a redundant warning. Erring early costs the tow. It now errs late.

Monday tickets on a curb posted no stopping 4 to 7pm: 34 at 4pm, 4 at 5pm, 1 at 6pm. The inferred end lands at 6 for a rule that runs to 7.
Thirty-four, four, one. The officer works the start of the window and moves on, so the tickets end an hour before the rule does.

And in the same week, a dataset I'd fetched and stored on day one and never read: 864 posted sweeping routes, each with a real start and end time. Every one of the 61,829 rules had been inferred from tickets while the posted answer sat one table over. The window end is exactly the thing tickets cannot tell you, and it had been there all along.

Aug 10

The city clerk had written it down

Council files quote sign text verbatim. That sounds obvious once you know it and I had no idea. A greppable index leads to a file number leads to a PDF with a text layer, and in the PDF is the city, in 1990, establishing a permit district "with six separate parking restrictions". Heterogeneity is the city's own word. It goes down to the side of the street: the west side of Gardner gets one scheme, the east side gets another the menu didn't even have.

One file settled something a drive had left open. Franklin is a boundary street. The council put its south side in one permit district and its north side in a different one. So the tempting shortcut, assume the dominant district for a segment that has no clear signal, would have asserted the wrong district's rules for a curb the city had specifically placed elsewhere. Franklin stays untouched until districts are assigned per curb rather than per block.

Aug 11 to 13

Which side of the street

The correction that changed the shape of everything. A rule does not belong to a block. It belongs to a curb, and the two curbs on one block routinely have different rules. On one stretch of Vermont the odd side drew 251 Friday sweeping tickets and the even side drew 192 Wednesday ones. Same block, opposite curbs, different days. Across every block where both curbs could be resolved, 87% are like that.

One block with two curbs, swept on different days, both geocoded to the same centerline point.
The part that hurt: the city geocodes every ticket to the middle of the street, so both curbs share a coordinate. The data cannot see the difference it is showing you.

I audited 37 blocks by hand, in Street View, over two days. The first two groups passed outright, 15 of 15 and 12 of 12, which is what let me loosen the gates. The third group failed nine times out of ten, and that failure was the valuable one. On a small share of streets the address ranges are attached to the wrong side, mirrored. When the house numbers on the tickets disagreed with the geometry, the house numbers were right, nine times out of ten, and there is now a guard that refuses to assert a side when the two contradict.

The audit is also where I learned to walk. The first time I read signs off Street View I read "THURSDAY" at two spots on Catalina and assigned them to opposite curbs, then built a whole schedule change story on top and committed it. Both readings were the west curb. I had turned the camera around without noticing that turning around swaps left and right. I caught that twice in one afternoon, and it became the first rule in the walking manual marked as law: never attribute a sign to a curb from the camera heading alone. Anchor it to what is physically behind the sign.

Every capture date is a different drive, and different dates were driven in different lanes. So "see more dates" is a side selector. The trick that made the whole verification method work, from the log.
Aug 12

Five blocks away

I parked near Melrose and the app announced 929 N Stanley, West Hollywood. Five blocks from where the car was.

The log explained it. The park had been frozen from a location fix that was 291 seconds old, taken at 11.7 meters per second, southbound. That's where the car was five minutes before I unplugged, and the detector had accepted it without looking at the age or speed sitting right there in its own log fields. Worse, the morning's park had gone 94 minutes without a single location fix. The app was being suspended mid-drive, and plug events during suspension were never delivered. It had been chronic for two days and nothing had said so.

How the park is found and what ends the belief: plug in and drive, unplug, check whether the car is still moving; the unplug spot or a five-minute rest becomes the park. The belief ends when the car moves again and is kept one step behind for Not my car. A killed app restores it on relaunch. Nothing expires on a timer.
What came out of Stanley: a belief that can be wrong needs a way to be uncertain about itself.

The fix has three parts and the first one is the design. At the unplug, the fix has to vouch for itself: recent, and not showing driving speed. A fix that can't vouch still creates the park, but flagged as position uncertain, and the first genuine fix during the settling window replaces the position outright, with the heading deliberately thrown away so the curb is refused rather than guessed from a direction the car held blocks earlier. If nothing better arrives, the announcement is the honest version: check the signs where you parked. No street named. No rules resolved at a place the car merely drove past.

The other two parts were plumbing that heals itself and instrumentation that stays on outside test sessions, so the next starvation names its own cause instead of leaving me to do forensics on fix ages.

Aug 12 to 14

A city inside the city

I checked a spot at 715 N San Vicente and the app had nothing, because I had crossed into West Hollywood without any way of knowing. LA's data stops at a border you cannot see from the car. Two cities hold centerlines for the same boundary streets, sometimes less than a meter apart, and the app had been quietly matching against whichever one it hit first.

So I went to find out what West Hollywood publishes. It is almost nothing. Forty-three datasets on the portal, and no citations, no meters, no curb paint, no permits. Street sweeping schedules exist as seven CAD drawings from 2008, one per weekday, exported from MicroStation with no text layer and no hours, seventeen years stale, and downloadable only through a browser because the city's edge network blocks everything else. The inference engine that had just produced 61,829 rules for LA had no fuel here whatsoever.

What unlocked it was staring at the maps long enough to notice that the color is painted on the curb that gets swept, not down the middle of the street. Every one of Monday's 107 strokes sits at distance 0.000 from a curb line. The maps had been telling us the side the whole time, in the only language a 2008 CAD export has.

Decoding all seven took a day. Five weekday maps with two different legend dialects, a commercial map with multi day zones, and the alleys, which turned out to have no schedule of their own because they follow whichever neighborhood they're in. Then a smaller discovery along the way: a meter decal on Curson listed Harvey Milk Day as a holiday, which LA does not observe, which sent me to audit every neighbouring city and find that Santa Monica keeps three different holiday lists for three different restriction types and Beverly Hills suspends permit parking on holidays while LA enforces it. Holidays are per city and per restriction class. Nobody tells you this.

West Hollywood shipped on the 14th. 698 segments, 653 sweeping rules read per curb, correct on every one of the 61 curbs I had personally stood at. The city went from a refusal message to an answer in two days, with a standing caveat that it only knows sweeping.

Part four

Part four: the walkers, now its own case study

From August 11 the story of the sign-reading agents is its own case study: The Agentic Walkers, told day by day.

Reading it back

What the log shows

Almost every turning point in it came from a drive or a walk. The bus stop that ended the detection approach. The spot check on San Vicente that found a city missing. The pole on La Fayette that overruled two confident diagnoses. Nothing in this month changed because of an argument.

The other thing, which I did not notice until I read it end to end, is how much of it is me being wrong in a recoverable way. A whole month of it. The reason the corrections were cheap is the six documents from the first week, because they meant I could tell a bug from a violated principle, and the second kind got fixed the same day every time.

And the thing I'd want someone hiring for this kind of work to take from it: the agent was right about a great deal and wrong about a fair amount, and the useful skill was never predicting which. It was building the conditions under which being wrong got noticed quickly, by a drive, by a sign, by another agent, or by me, before anyone got a ticket.

Three questions the project can be asked. Two have measured answers. The third, whether the behavior works on a person, is an empty box.
What is checked, and the one thing that still is not. Two of these have answers behind them. The empty box is the honest state of the third.