A channel that points at the organisation, not at the resident - #149
Merged
Conversation
…he resident The report form has offered exactly two destinations: a dripping tap to the maintenance board, a roommate conflict to the incident ladder. An objection to how AOZ itself acted fitted neither — and filing one as an Incident would have been worse than dropping it, because that ladder escalates TOWARD a resident and ends in FORMAL_MEASURE. Complaining about staff would have opened a case against the person complaining. The City's Eigentümerstrategie 2025-2028 fixes "Information und Beschwerdestellen" as one of six minimum standards in AOZ's Leistungsauftrag. AOZ's own central Beschwerdestelle logged 88 complaints in 2023, 145 in 2024 and 242 in 2025 — 38% of the last figure about Unterbringung und Zusammenleben, while the client count stayed flat. The product had no side of that obligation at all. THE BOUNDARY IS THE FEATURE. `complaints:read` and `complaints:respond` are held by no care role, and — the part that matters — are NOT widened by ALL_DOMAINS. Every other permission grows with oversight, because seeing every domain is the point of that axis. These must not: the person with oversight over every care domain is one of the people a complaint can be ABOUT. A grievance channel whose reader may be its subject is not a grievance channel. They are named separately from SYSTEM_ADMIN_PERMISSIONS even though isSystemAdmin grants both today, because configuring the product and hearing a complaint against staff are different jobs. AOZ's Beschwerdestelle is deliberately independent of the site team; when a real person fills that seat they should get these two verbs without also being handed the settings page, which is the whole lesson of retiring ADMIN. Anonymity is offered, with its cost stated where the choice is made: no name means nobody can answer, and it will not appear under "Deine Meldungen". `residentId` is nullable and the null IS the anonymity — there is nothing to redact later because nothing was written. The route calls no logAudit for the same reason: an audit row naming the reporter would quietly undo the promise. Resident-facing copy is translated into all six offered locales. The first version used the staff German constants and three portal gates caught it, rightly — a complaints channel that only speaks German is close to useless for the people it exists to protect, who are the least likely in the building to read it. The i18n script gate then caught a Ukrainian apostrophe (U+02BC where that dictionary uses U+2019), which is the same sibling-language class as the `ru` file that was one-third Ukrainian. Gated by complaint-boundary.test.ts. Mutation-proven: removing the early return in `hasPermission` and granting `complaints:read` to BETREUUNG — the exact "tidy up the special case" edit someone will eventually attempt — fails three checks. Verified with SESSION_SECRET=… npm run build (exit 0); all three routes compile. Contains a migration. 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 report form has offered exactly two destinations: a dripping tap to the maintenance board, a roommate conflict to the incident ladder. An objection to how AOZ itself acted fitted neither.
And filing one as an
Incidentwould have been worse than dropping it — that ladder escalates toward a resident and ends inFORMAL_MEASURE. Complaining about staff would have opened a case against the person complaining.The City's Eigentümerstrategie 2025–2028 fixes „Information und Beschwerdestellen" as one of six minimum standards in AOZ's Leistungsauftrag. AOZ's own central Beschwerdestelle logged 88 → 145 → 242 complaints across 2023–2025, 38% of the last figure about Unterbringung und Zusammenleben — while the client count stayed flat. The product had no side of that obligation.
The boundary is the feature
complaints:readandcomplaints:respondare held by no care role, and — the part that matters — are not widened byALL_DOMAINS.Every other permission in this product grows with oversight, because seeing every domain is the point of that axis. These must not: the person with oversight over every care domain is one of the people a complaint can be about. A grievance channel whose reader may be its subject is not a grievance channel.
They're named separately from
SYSTEM_ADMIN_PERMISSIONSeven thoughisSystemAdmingrants both today, because configuring the product and hearing a complaint against staff are different jobs. AOZ's Beschwerdestelle is deliberately independent of the site team; when a real person fills that seat they should get these two verbs without also being handed the settings page — the whole lesson of retiringADMIN.Anonymity, with its cost stated
residentIdis nullable and the null is the anonymity — there is nothing to redact later because nothing was written. The route calls nologAuditfor the same reason: an audit row naming the reporter would quietly undo the promise the form makes.The trade-off sits next to the checkbox rather than buried: anonymous means nobody can answer, and it won't appear under „Deine Meldungen". Burying that would let someone choose anonymity and then wait for a reply that can never arrive.
Two gates caught me
rufile that was once one-third Ukrainian.Mutation-proven
Removing the early return in
hasPermissionand grantingcomplaints:readtoBETREUUNG— the exact "tidy up the special case" edit someone will eventually attempt — fails three checks.202 suites / 3563 passed.
SESSION_SECRET=… npm run buildexit 0, all three routes compile. Contains a migration.🤖 Generated with Claude Code
https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h