Count who needs what, without keeping a register of why - #148
Merged
Conversation
… of why The City's Eigentümerstrategie 2025-2028 fixes "Berücksichtigung von vulnerablen Personen" as one of six minimum standards in AOZ's Leistungsauftrag, and the Gemeinderat asked specifically for the "systematische Erfassung der Anzahl vulnerabler Geflüchteter". AOZ answered both on paper — a medical vulnerability assessment form in 2025, plus a separate project to record clients "mit eingeschränkten Wohnfähigkeiten sowie insbesondere ältere Geflüchtete" to close "bestehende Angebotslücken im Bereich Wohnen und Wohnbegleitung". None of it was answerable from a system. The obvious implementation is a `vulnerable Boolean` or a `vulnerabilityType` enum on Resident. Both are wrong, for two reasons that point the same way. ETHICALLY: a stored category is applied TO someone and outlives its reason. In this context "vulnerable" reaches for exactly what this product is forbidden to hold — a diagnosis, a violence history, a status. So vulnerability is DERIVED from housing facts the caseworker already recorded because a PLACEMENT needed them: a ground floor, a single room, quiet, space for equipment. Reading them together adds no new fact about the person; it answers a question the City is entitled to ask. STRUCTURALLY: a stored flag is a second copy of state living in seven columns, and the two will drift. Then the reportable figure and the person's own record disagree, with no way to tell which is right. `assessVulnerability` returns the GROUNDS, not a verdict — AOZ has to defend this number, and "the system says 6" is not a defence. Same rule the compatibility scoring already follows: no black-box outcomes. One genuinely missing fact is now storable: `livingSkillsSupport` (Wohnfähigkeit). It is NOT a second `supportLevel` — that is contact FREQUENCY, this is everyday competence with cooking, post, appointments and keeping a tenancy. They come apart in both directions, which is why one cannot stand in for the other, and why AOZ was recording it in a separate project. Defaults to INDEPENDENT so no existing row acquires a need nobody assessed: an unasked question must not read as an answer. Config-driven as CLAUDE.md requires — the factor is one entry in RESIDENT_FACTORS plus a column, and the intake form, the AI field registry and the zod schema all derive from it. The /analytics block reports the total WITH its breakdown, and says out loud that the rows sum to more than the headline because anyone with two needs appears twice — a reader who adds them up would overstate the caseload. Gated by vulnerability.test.ts, which holds the ethical line by DENYLIST: the module may read no field matching diagnos/permit/asylum/trauma/religion, no ground label may name a cause rather than a need, every ground must state a housing implication, and no `vulnerable` column may appear in the schema. Mutation-proven: adding `medicalDocType` to the input or renaming a label to "Gehbehinderung" each fail it. design-system.test.ts caught the org name hardcoded in one ground's copy during development — kept the rewrite, which says what to do rather than who is counting. Verified with SESSION_SECRET=… npm run build (exit 0). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The City's Eigentümerstrategie 2025–2028 fixes „Berücksichtigung von vulnerablen Personen" as one of six minimum standards in AOZ's Leistungsauftrag, and the Gemeinderat asked specifically for the „systematische Erfassung der Anzahl vulnerabler Geflüchteter".
AOZ answered both on paper — a medical vulnerability assessment form in 2025, plus a separate project to record clients „mit eingeschränkten Wohnfähigkeiten sowie insbesondere ältere Geflüchtete" in order to close „bestehende Angebotslücken im Bereich Wohnen und Wohnbegleitung". None of it was answerable from a system.
The design decision
The obvious implementation is
vulnerable BooleanorvulnerabilityTypeon Resident. Both are wrong, for two independent reasons that happen to agree.Ethically — a stored category is applied to someone and outlives its reason. In the asylum context "vulnerable" reaches for precisely what this product is forbidden to hold: a diagnosis, a violence history, a status. So vulnerability is derived from housing facts the caseworker already recorded because a placement needed them — a ground floor, a single room, quiet, space for equipment. Reading them together adds no new fact about the person; it answers a question the City is entitled to ask.
Structurally — a stored flag is a second copy of state that already lives in seven columns, and the two will drift. Then the reportable figure and the person's own record disagree, with no way to tell which is right.
assessVulnerabilityreturns the grounds, not a verdict. AOZ has to defend this number, and "the system says 6" is not a defence; "6, because three need a ground floor, two cannot share and one needs daily help" is.The one genuinely missing fact
livingSkillsSupport— Wohnfähigkeit. It is not a secondsupportLevel: that is contact frequency, this is everyday competence with cooking, post, appointments, keeping a tenancy. They come apart in both directions, which is exactly why AOZ was recording it in a separate project rather than reading it offsupportLevel.Defaults to
INDEPENDENT, so no existing row acquires a support need nobody assessed — an unasked question must not read as an answer.Config-driven per CLAUDE.md: one entry in
RESIDENT_FACTORSplus a column, and the intake form, the AI field registry and the zod schema all derive from it.Reporting
The
/analyticsblock gives the total with its breakdown, and says out loud that the rows sum to more than the headline because anyone with two needs appears twice — a reader who added them up would overstate the caseload.The gate
vulnerability.test.tsholds the ethical line as a denylist, which is the half that catches the field nobody thought of:diagnos/permit/asylum/trauma/religion/pregnanvulnerablecolumn may appear in the schemaMutation-proven: adding
medicalDocTypeto the input, or renaming a label to „Gehbehinderung", each fail it.design-system.test.tsalso caught me hardcoding the org name in one ground's copy during development. Kept the rewrite — it now says what to do rather than who is counting.201 suites / 3548 passed. Verified with
SESSION_SECRET=… npm run build(exit 0).Contains a migration — adds the
LivingSkillsSupportenum and column.🤖 Generated with Claude Code
https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h