TAGS: ai, architecture, showdev, javascript
Here is the thing we actually ship: a single PDF packet, 30-odd pages, that a small public housing authority hands to its board and its auditor.
Front half is a redline. Their own ACOP (Admissions and Continued Occupancy Policy) and HCV Administrative Plan chapters, with the HOTMA-driven changes marked up and the agency's discretionary elections filled in — asset thresholds, hardship relief, de minimis error policy. Back half is the procedural record that proves the redline was adopted properly: the public notice, the Resident Advisory Board agenda and consultation memo, the hearing script, the board resolution, and a dated adoption memo.
The redline is the part people expect to be hard. It wasn't. The calendar was.
Six documents that quote each other
None of these documents stand alone. The resolution recites the hearing date. The hearing script cites when the notice was published and when comments closed. The adoption memo cites the resolution number and the vote date. The RAB memo cites the consultation meeting and the comment window it sits inside.
So a packet with one wrong date isn't a packet with one typo. It's a packet where the resolution attests to a hearing that, according to the notice, hadn't happened yet. That's exactly the kind of thing an auditor pulls a thread on.
The fix isn't better prompting. It's refusing to let dates be prose at all.
Milestones declare constraints, not dates
Every artifact in the packet declares what it depends on and by how much. Nothing declares when it happens.
const MILESTONES = [
{ id: 'notice', after: [] },
{ id: 'rab_consult', after: [{ of: 'notice', minDays: 10 }] },
{ id: 'comment_close', after: [{ of: 'notice', minDays: 45 }] },
{ id: 'hearing', after: [{ of: 'comment_close', minDays: 0 },
{ of: 'rab_consult', minDays: 0 }] },
{ id: 'board_vote', after: [{ of: 'hearing', minDays: 0 }],
onlyOn: agency.boardCalendar },
{ id: 'effective', after: [{ of: 'board_vote', minDays: 0 }] },
]
The minDays values are config, not constants. Some come from federal rules, some from the agency's own bylaws, and they differ per agency — which is the whole reason they're data.
onlyOn is the interesting field. A board doesn't vote whenever the math says it can. It votes on its regular meeting schedule: third Tuesday, or the second Thursday of odd months, or whatever the bylaws say.
Why one backward pass isn't enough
Solving forward from today is easy. Solving backward from a fixed effective date is what the customer actually wants, because there's a wall at 1 January 2027 and the question is "when do we have to start."
Backward looks like simple subtraction until onlyOn shows up. Subtract from the effective date, land on 14 October, discover the board doesn't meet on the 14th, snap back to the 8th. You just moved the vote six days earlier — which means the hearing has to move, which means comments close earlier, which means the notice has to publish earlier. And the RAB consultation, which was comfortably satisfied at the old dates, may now be sitting on the wrong side of its own 10-day floor.
One pass gives you a schedule that looks right and quietly violates a constraint it satisfied before the snap. So the solver iterates:
let dates = seedBackward(MILESTONES, effectiveDate)
for (let i = 0; i < MAX_ITERS; i++) {
const next = relaxBackward(MILESTONES, dates) // re-apply every edge, re-snap onlyOn
if (sameDates(next, dates)) return { ok: true, dates }
dates = next
}
return { ok: false, reason: 'no fixpoint' }
Each pass only ever moves dates earlier, so it's monotone and it terminates. It's a small fixpoint loop, not a real constraint solver, and that's fine — the graph has six nodes.
Infeasibility is a result, not an error
The loop can push notice into the past. That's not a crash. It's the single most useful thing the packet can tell a small PHA in late 2026: you cannot adopt this on your normal meeting calendar.
So that branch doesn't throw. It renders a different packet — one built around a special meeting, with the notice-of-special-meeting document swapped in and the resolution's recitals rewritten to reference the bylaw provision that authorizes it. Same elections, different procedural path.
Prose last
Once dates exist, generation is narrow. The model's job is to pick the discretionary election given the agency's profile and write the reasoning around it. Every date and every cross-reference is a token substituted from the solved schedule after the fact. The resolution and the hearing script can't disagree, because neither of them ever wrote a date.
That's the reusable bit. If your output is several documents that cite each other, the citations belong in a data structure that gets solved once, not in six generations you then hope agree.
This is how we built QuorumFile — flat-fee HOTMA adoption packets for small PHAs: https://quorumfile.kynth.studio
Top comments (0)