Public: anyone can read this; the crew can change it.

The Fair Housing check catches 'not suitable for children' and 'couples welcome', and a phrase table proves what it catches and what it clears #33

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/listing-launch:mason/31-the-fair-housing-check-catches-not-suita → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/31-the-fair-housing-check-catches-not-suita was merged into main as 5847384.

Review

Sign in to review this change.
3 files changed +75 −1
README.md +2 −1
@@ -116,7 +116,8 @@ MLS, or edit images. You copy, print and send. The Fair Housing check is a
116116 second pair of eyes, not legal advice: it knows phrases in context
117117 (familial status, religion, national origin, disability, race, sex) —
118118 "family room" passes, "family-friendly" does not; "integrated appliances"
119 passes, "integrated neighborhood" does not; "master bedroom" is a
119 passes, "integrated neighborhood" does not; "not suitable for children" and
120 "couples welcome" do not; "master bedroom" is a
120121 flag-with-note — and a person approves every word.
121122
122123 ## For whoever runs it
src/fairhousing.test.ts +55
@@ -93,3 +93,58 @@ test("a finding carries a drop-in phrase only where the rule's rewrite can stand
9393 expect(check("A Christian home.").findings[0]?.dropIn).toBeNull();
9494 expect(check("Perfect for a young family.").findings[0]?.dropIn).toBeNull();
9595 });
96
97 test("a phrase table: what must be flagged, with its class, and what must stay clear", () => {
98 const flag: [string, string, "risk" | "note"][] = [
99 ["Not suitable for children.", "familial status", "risk"],
100 ["Unsuitable for kids.", "familial status", "risk"],
101 ["Not for children.", "familial status", "risk"],
102 ["No children.", "familial status", "risk"],
103 ["Not ideal for young families.", "familial status", "risk"],
104 ["Couples welcome.", "familial status", "risk"],
105 ["Perfect for couples.", "familial status", "risk"],
106 ["Great for young couples.", "familial status", "risk"],
107 ["A childless household preferred.", "familial status", "risk"],
108 ["No families.", "familial status", "risk"],
109 ["Family-friendly street.", "familial status", "risk"],
110 ["Kid-friendly yard.", "familial status", "risk"],
111 ["No kids.", "familial status", "risk"],
112 ["Adults only.", "familial status", "risk"],
113 ["Perfect for empty nesters.", "familial status", "risk"],
114 ["Near the church.", "religion", "risk"],
115 ["A Christian home.", "religion", "risk"],
116 ["A Hispanic neighborhood.", "national origin", "risk"],
117 ["English-speaking only.", "national origin", "risk"],
118 ["A white neighborhood.", "race", "risk"],
119 ["A safe neighborhood.", "race", "risk"],
120 ["Not suitable for the disabled.", "disability", "risk"],
121 ["Handicapped accessible.", "disability", "note"],
122 ["Bachelor pad.", "sex", "risk"],
123 ];
124 for (const [text, touches, level] of flag) {
125 const f = check(text).findings[0];
126 expect([text, f?.touches, f?.level]).toEqual([text, touches, level]);
outdated

Medium: the expected level is computed from the actual one (f?.level === "note" ? "note" : "risk"), so this assertion can never fail on level. A rule that dropped from risk to note, or a flag that is only a note, would pass. Put the expected level in the table, e.g. [text, touches, level] tuples ("risk" for all except the real notes), and compare f?.level against it.

127 expect(f?.line).toContain("; say '");
128 }
129 for (const text of [
130 "A family room.",
131 "Walk-in closets.",
132 "Integrated appliances.",
133 "On Church Street.",
134 "A quiet street.",
135 "Within walking distance of shops.",
136 "Great schools nearby.",
137 "Spanish-style home.",
138 "Colonial home.",
139 "A 55+ community with senior living amenities.",
140 "Two bedrooms for the whole household.",
141 "Not far from the park, for kids and adults to enjoy.",
142 "Not for sale before March.",
143 ])
144 expect([text, flagged(text)]).toEqual([text, []]);
145 });
146
147 test("the new familial-status rules are advice: no drop-in", () => {
148 for (const text of ["Not suitable for children.", "Couples welcome.", "Childless.", "No families."])
149 expect([text, check(text).findings[0]?.dropIn]).toEqual([text, null]);
150 });
src/fairhousing.ts +18
@@ -120,6 +120,24 @@ export const RULES: readonly Rule[] = [
120120 why: "excludes households with children (unless a qualified 55+ community)",
121121 say: "describe the property; drop the restriction",
122122 },
123 {
124 pattern: rx(
125 "(?:not (?:suitable|suited|appropriate|recommended|ideal|safe)? ?for|unsuitable for) (?:the |any |young |small )*(?:children|kids|toddlers|babies|families)",
126 ),
127 touches: "familial status",
128 level: "risk",
129 why: "states a preference against households with children",
130 say: "describe the property: 'steep stairs and an unfenced pond'",
131 },
132 {
133 pattern: rx(
134 "(?:couples?|singles?|adults?|professionals?) (?:only|welcome|preferred|wanted)|childless|no families",
135 ),
136 touches: "familial status",
137 level: "risk",
138 why: "states a preference by household",
139 say: "describe the home, not who should live there",
140 },
123141 {
124142 pattern: rx(
125143 "empty[- ]nesters?|newlyweds?|young (?:couples?|professionals?)|singles|single (?:person|man|woman|professional)s?|mature (?:couples?|persons?|adults?)",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs