Skip to content

Count who needs what, without keeping a register of why - #148

Merged
catomean merged 1 commit into
masterfrom
feat/vulnerability-attributes
Sep 1, 2026
Merged

Count who needs what, without keeping a register of why#148
catomean merged 1 commit into
masterfrom
feat/vulnerability-attributes

Conversation

@catomean

@catomean catomean commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

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 Boolean or vulnerabilityType on 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.

assessVulnerability returns 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 second supportLevel: 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 off supportLevel.

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_FACTORS plus a column, and the intake form, the AI field registry and the zod schema all derive from it.

Reporting

The /analytics block 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.ts holds the ethical line as a denylist, which is the half that catches the field nobody thought of:

  • the module may read no field matching diagnos / permit / asylum / trauma / religion / pregnan
  • no ground label may name a cause rather than a need
  • every ground must state a housing implication — a ground without one is a label on a person
  • 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 also 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 LivingSkillsSupport enum and column.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h

… 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
@catomean
catomean merged commit 6baeaaf into master Sep 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant