diff --git a/.gitignore b/.gitignore index 593fc4c..735aada 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,8 @@ playwright/.auth/ # personal documents kept alongside the repo, not part of the app Org Docs/ just-tech-application/ + +# non-code working files that name individuals or record private conversations +# (see ops/AGENTS.md — this is a safety rule, not tidiness) +ops/private/* +!ops/private/README.md diff --git a/AGENTS.md b/AGENTS.md index 600f78a..87d1011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ Directory-specific rules live in nested `AGENTS.md` files and override this one | [`src/lib/AGENTS.md`](src/lib/AGENTS.md) | Scoring, survey items, validation — the load-bearing logic | | [`src/components/AGENTS.md`](src/components/AGENTS.md) | Astro vs React, brand tokens, islands | | [`migrations/AGENTS.md`](migrations/AGENTS.md) | Schema changes and the production migration trap | +| [`ops/AGENTS.md`](ops/AGENTS.md) | Non-code work: growth, funding, partnerships, content | > **Never put an `AGENTS.md` (or any `.md`) inside `src/pages/`.** Astro's file-based > routing turns Markdown in `src/pages/` into a public page — a guide at @@ -82,6 +83,7 @@ npm test -- scoring # filter by name npm run build # production build npm run e2e # fresh local D1 + seed + build + Playwright npm run db:setup # db:fresh then db:seed (local D1 only) +npm run ops:metrics # regenerate ops/METRICS.md from production (read-only) ``` `npm run smoke` has no default target. Supply an explicit `--environment` and diff --git a/ops/AGENTS.md b/ops/AGENTS.md new file mode 100644 index 0000000..fbe9610 --- /dev/null +++ b/ops/AGENTS.md @@ -0,0 +1,72 @@ +# `ops/` — Non-Code Work + +Growth, funding, partnerships, and content. `.planning/` is engineering milestones; this is +everything else. Start at [`README.md`](README.md). + +--- + +## Hard rules + +**1. Never write a named individual into a tracked file.** + +Organizations, yes. People, no. No names, no email addresses, no "she said," no notes from +a private conversation. Those go in `ops/private/`, which is gitignored. + +This is a safety rule, not a style preference. RateMyPlace's first non-negotiable is +protecting people from retaliation, and the tenant organizers and legal aid staff who help +the project are covered by the same logic as reviewers. If you are drafting outreach +content and need a placeholder, write the role: "the tenant union's organizing lead." + +**2. Never hand-type a number that can be queried.** + +`ops/METRICS.md` is generated by `scripts/ops-metrics.ts` from the production database. +Do not edit it. Do not copy its figures into other files either — link to it. A number +copied into prose is a number that will be wrong within a month, and wrong numbers in a +grant application are worse than absent ones. + +If you need a figure that the script doesn't produce, add it to the script. + +**3. Aspiration and fact are labeled, always.** + +`MASTER.md` spent months describing fraud controls, velocity alerts, and honeypots that +were never built, because nobody separated the plan from the state. It now uses a +**Built today / Planned** split. Use the same discipline here: a channel that was *tried* +reads differently from one that was *considered*. + +**4. Voice: no em dashes.** + +`brand.md` v1.4 removed em dashes throughout for consistency with the brand bible. Anything +here that could become public copy follows that. Use commas, colons, or a full stop. + +See [`content/MESSAGES.md`](content/MESSAGES.md) for approved language, and `brand.md` §2 +for the full voice guidance. + +## When updating a file + +- Update the `Last reviewed:` date. If you didn't verify it, don't touch the date. +- Record outcomes, not intentions. "Emailed three tenant unions, one replied, meeting set" + is useful. "Plan to reach out to tenant unions" ages into noise. +- Failures are the valuable entries. A channel that didn't work saves the next person + the attempt, and it is the thing nobody writes down. + +## Where the truth lives + +| Question | Source of truth | +|---|---| +| How many reviews, buildings, landlords? | `ops/METRICS.md` (generated) | +| What does the product actually do? | `MASTER.md` — Built today / Planned split | +| How do we talk about it? | `brand.md` §2, then `ops/content/MESSAGES.md` | +| What is the scoring methodology? | `src/lib/scoring.ts`, published at `/methodology` | +| What is the engineering state? | `.planning/`, then `git log` | + +When a document and the code disagree, **the code wins** — root `AGENTS.md` says so, and it +has been right every time it came up. + +## Don't + +- Don't promise capability the product doesn't have. Every claim in outreach or a grant + narrative must be checkable against `MASTER.md`'s Built section or `ops/METRICS.md`. +- Don't overstate the health weighting. Its real-world effect on scores is under 0.1 in + practice. Saying otherwise is the exact overselling this project exists to avoid. +- Don't create a tracking file for work that isn't happening. An empty template is worse + than nothing; it looks like a system and holds no information. diff --git a/ops/METRICS.md b/ops/METRICS.md new file mode 100644 index 0000000..54e10c1 --- /dev/null +++ b/ops/METRICS.md @@ -0,0 +1,55 @@ +# Metrics + +**Generated 2026-08-29 from the production database.** Do not edit by hand — run: + +```bash +npx tsx scripts/ops-metrics.ts +``` + +Read-only. Every query is a SELECT. + +## The number that matters + +**53 approved reviews.** + +Everything else in [growth/](growth/STRATEGY.md) exists to move this. The site is +methodologically sound and technically healthy; it is thin on evidence. + +## Coverage + +| Measure | Count | +|---|---| +| Approved reviews | 53 | +| Pending review queue | 0 | +| Verified reviews (proof of address) | 15 | +| Buildings in database | 91 | +| Buildings with at least one review | 51 | +| Buildings with no review yet | 40 | +| Landlords | 27 | +| Property managers | 0 | +| Disputes filed | 0 | + +51 of 91 buildings carry a review. A search that returns an +empty building is a visitor who leaves. + +## Landlord score coverage + +A landlord aggregate is withheld below 3 approved reviews, so a +named party is never scored on thin evidence. + +| Approved reviews | Landlords | Score | +|---|---|---| +| 1 | 21 | withheld | +| 2 | 4 | withheld | +| 4 | 1 | shown | +| 5 | 1 | shown | + +**2 landlords currently show a score. 25 are withheld.** + +This is the clearest statement of the constraint. The threshold is a deliberate fairness +choice, not a bug — but it means landlord pages only become useful with volume. + +--- + +*Regenerated by `scripts/ops-metrics.ts`. If this file's date is old, the numbers are +old — rerun it before quoting any figure in a grant application, a pitch, or a post.* diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 0000000..e0cfcff --- /dev/null +++ b/ops/README.md @@ -0,0 +1,61 @@ +# ops/ — the non-code work + +Everything RateMyPlace needs that isn't source code: getting reviews, funding the work, +building relationships, and saying what the project is. + +`.planning/` tracks engineering milestones. This tracks everything else. + +## Why this exists + +The site is technically healthy and methodologically sound. It is thin on evidence: +**53 approved reviews**, and only 2 of 27 landlords clear the three-review threshold +required to show a score. No code change fixes that. This directory is where that work +lives. + +## What's here + +| Path | Holds | Refresh | +|---|---|---| +| [`METRICS.md`](METRICS.md) | The real numbers, straight from production | **Generated** — never edit by hand | +| [`growth/`](growth/STRATEGY.md) | Getting reviews. The priority. | When something is tried or learned | +| [`funding/`](funding/PIPELINE.md) | Opportunities, deadlines, reusable narrative | When a deadline moves or a piece of prose proves itself | +| [`partners/`](partners/LANDSCAPE.md) | Organizations and the landscape | When the map changes | +| [`content/`](content/MESSAGES.md) | Approved language and talking points | When a message is tested | +| `private/` | Named individuals, private conversations, funder feedback | **Gitignored. Never tracked.** | + +## The privacy rule + +**Named individuals and private conversations go in `private/`, which is gitignored.** + +This is not bureaucratic. RateMyPlace's first non-negotiable is that people who talk to it +should not face retaliation, and that extends past reviewers to the tenant organizers, +legal aid staff, and city contacts who help. A public list of who spoke to you and what +they said is exactly the artifact this project exists to argue against. + +Tracked files may name **organizations**. They may not name **individuals**, quote private +conversations, or record what someone said off the record. + +## Keeping it current + +Every file carries a `Last reviewed:` date. That is a claim, and it can be wrong. + +Two rules, both learned the hard way in this repo — `VERSION.md` sat frozen at +v1.1.0-alpha for seven months, and `MASTER.md` described fraud controls that were never +built: + +1. **Numbers are generated, never typed.** Anything countable comes from + `npx tsx scripts/ops-metrics.ts`. If you find yourself typing a figure into a document, + it will be wrong within a month. +2. **When a document and reality disagree, reality wins** — and the document gets fixed in + the same sitting, not added to a list. + +Agents maintaining these files: read [`AGENTS.md`](AGENTS.md) in this directory first. + +## Refreshing the numbers + +```bash +npx tsx scripts/ops-metrics.ts +``` + +Read-only against production. Run it before quoting any figure in an application, a pitch, +or a post. diff --git a/ops/content/MESSAGES.md b/ops/content/MESSAGES.md new file mode 100644 index 0000000..10220d1 --- /dev/null +++ b/ops/content/MESSAGES.md @@ -0,0 +1,106 @@ +# Messages + +**Last reviewed:** 2026-08-29 + +Approved language. Voice rules come from `brand.md` §2; this is where they get applied to +specific recurring situations. + +**Voice rules that apply to everything below:** + +- **No em dashes.** `brand.md` v1.4 removed them for consistency with the brand bible. + Use commas, colons, or a full stop. +- Plain, direct, civic. Specific over clever. Active voice. +- Write from the tenant's side of the screen. +- No urgency, no marketing register, nothing that sounds like a startup pitch. +- Credibility comes from restraint and from showing the work. + +## The canonical line + +> **A public record of rental housing, from the people who know it best.** + +This is the primary message, from the brand bible. Do not paraphrase it into something +punchier. The shorter variant "a public record for renters" is retired as a primary line +and survives only as a mid-sentence descriptor. + +## Positioning, when someone asks what it is + +> We are not Yelp for apartments. We are closer to a public health department with a +> comment section. + +Useful because it does two jobs at once: it sets the frame and it heads off the comparison +everyone reaches for first. + +## The ask, to a prospective reviewer + +Lead with the next tenant, not with civic good. A review does not help the person writing +it, and pretending otherwise is transparent. + +> Your review will not help you. You already know what living there was like. It will help +> the person standing in that apartment next year deciding whether to sign. + +Follow with the honest cost: it is a 32-item survey and takes real time. Do not soften +that. People who are told it is quick and find it is not will abandon it, and abandoned +reviews are worse than declined ones. + +## On anonymity, which is the first question every time + +> Reviews are anonymous. We collect exact dates so the data is useful, and we never display +> them: the site shows a season and a year, or a range like "1 to 3 years ago." Your name +> is never shown. Reviews are moderated before publication, and landlords can dispute a +> review through a formal process, but a review is never removed because a landlord +> disagrees with it. + +Every clause here is true and checkable. Do not add reassurance beyond it. + +**Do not say "we don't store IP addresses."** It is not accurate. Reviewer IPs are never +stored, but IPs are persisted for rate limiting and in admin audit logs. If pressed, say +exactly that. + +## On methodology, when credibility is the question + +> The survey is adapted from three validated public health instruments rather than +> invented: the Observational Housing Quality Scale, the Physical Housing Quality Scale, +> and the WHO LARES study. Every item, every weight, and every citation is published at +> ratemyplace.org/methodology, including worked examples on real reviews. You can check +> our work. + +The methodology page is the strongest asset the project has. Point at it early and often. + +## On the health weighting, carefully + +Health and safety items carry more weight: pests and mold at 1.5x, structural and climate +at 1.3x, plumbing and building security at 1.2x. + +**Do not overstate the effect.** On real data the weighting shifts scores by less than 0.1 +in practice. If asked directly: + +> The weighting reflects which conditions the research says matter most for health. On our +> current dataset it moves scores only slightly. We publish the formula and the worked +> examples so anyone can see exactly how much it does. + +Overselling this is the precise failure the project's own ethics forbid. + +## On the size of the dataset + +Do not hide it and do not apologize for it. + +> The site is early. It holds [N approved reviews — see ../METRICS.md] across Boston and +> New Haven, and most landlords do not yet have enough reviews to show a portfolio score. +> We withhold a landlord's score below three reviews rather than publish a number based on +> one person's experience. + +The withholding rule is a strength worth stating. It says the project would rather show +nothing than something unfair, which is the opposite of what people expect from a review +site. + +**Never hard-code the count.** Run `npx tsx scripts/ops-metrics.ts` and fill it at use time. + +## Things not to say + +| Don't | Because | +|---|---| +| "We don't store IP addresses" | Not accurate. See above. | +| "Verified reviews" as a headline claim | Only some are. Unverified reviews are simply unbadged, never labeled unverified. | +| Anything implying we solve affordability | Explicitly out of scope, and claiming it undermines everything else. | +| "Rate My Professor for landlords" | Invites the consumer-review frame the project is built against. | +| Any number not freshly generated | See METRICS.md. | diff --git a/ops/funding/NARRATIVE-BLOCKS.md b/ops/funding/NARRATIVE-BLOCKS.md new file mode 100644 index 0000000..c6050e0 --- /dev/null +++ b/ops/funding/NARRATIVE-BLOCKS.md @@ -0,0 +1,111 @@ +# Narrative blocks + +**Last reviewed:** 2026-08-29 + +Reusable prose for grant applications, fellowship materials, and partner pitches. Written +once, checked against reality, reused rather than rewritten under deadline. + +**Rules for this file:** + +- Every factual claim must be checkable against `MASTER.md`'s **Built today** section or + [`../METRICS.md`](../METRICS.md). +- **Never hard-code a number here.** Write `[N approved reviews — see METRICS.md]` and fill + it at use time from a fresh generator run. Numbers in reusable prose are how a stale + figure ends up in a submitted application. +- No em dashes (`brand.md` v1.4 voice rule). + +--- + +## The problem (short, ~60 words) + +Housing is a social determinant of health. Leaks, pests, poor ventilation, and inadequate +heat drive asthma, cardiovascular disease, injury, and psychological distress, and they +fall hardest on low-income renters and communities of color. Yet rental markets run on +lopsided information: landlords pull credit reports and tenant blacklists, while tenants +get a walkthrough and a signature line. + +## The problem (long, ~130 words) + +Housing conditions are a documented driver of health outcomes. Substandard conditions are +associated with respiratory disease, cardiovascular problems, injury, and psychological +distress, and the burden is patterned along race, income, and immigration status. + +The information asymmetry in rental markets compounds this. A landlord evaluating a tenant +can access credit history, eviction records, and commercial screening reports. A tenant +evaluating a landlord has a walkthrough, a listing, and whatever they can learn from +strangers. Problem landlords can therefore operate across a portfolio without their +pattern becoming visible to the people most affected by it. + +Existing review sites treat housing as a consumer product and ask whether an apartment was +liked. That question does not surface the conditions that make people sick. + +## What RateMyPlace is (~70 words) + +RateMyPlace is a public record of rental housing, built from tenant reviews and structured +around validated public health instruments. Renters rate their unit, their building, and +their landlord across 27 scored items adapted from the Observational Housing Quality Scale, +the Physical Housing Quality Scale, and the WHO LARES study. Reviews are anonymous, +moderated, and disputable. Scores are published separately by domain, with the full +methodology public. + +## Theory of change (~50 words) + +If tenants can access structured, comparable information about units, buildings, and +landlords, then they can make more informed housing decisions, which reduces exposure to +substandard housing, leading to better health outcomes and greater landlord +accountability. Aggregated across a portfolio, the same data supports tenant organizing +and policy advocacy. + +## What distinguishes it (~90 words) + +Three things. + +The instrument is adapted from validated public health research rather than invented, so +each item maps to a documented health pathway. The methodology is published in full, +including every item, every weight, and every citation, so the scoring can be criticized +rather than merely trusted. And scores aggregate to the landlord and property manager, not +just the address, which is what makes a pattern of neglect visible across a portfolio. + +The design treats tenant anonymity as a safety requirement rather than a preference, +because retaliation is the risk that keeps people quiet. + +## Current state, told honestly (~80 words) + +The platform is live at ratemyplace.org and technically mature: server-rendered, deployed +on Cloudflare, with automated tests, moderation tooling, an audit trail, and a formal +landlord dispute process. + +It is early on evidence. It currently holds [N approved reviews — see METRICS.md] across +Boston and New Haven, and only [N — see METRICS.md] landlords have enough reviews to clear +the three-review threshold required before a portfolio score is displayed. Building the +review base is the present work, and it is a distribution problem rather than a technical +one. + +## On limitations (~60 words) + +RateMyPlace cannot address housing affordability, supply, or discrimination, and does not +claim to. It depends on tenants choosing to contribute, which introduces selection effects +that a small dataset cannot correct for. The health-based weighting shifts scores only +slightly in practice. These limits are documented publicly rather than managed around, +because a tool that overstates itself cannot be trusted with this subject. + +## Who built it (~40 words) + +RateMyPlace is built and maintained by a single person with a public health background, +working on it independently. That constrains pace and reach, and it is a reason support +would change what the project can do rather than merely how fast it moves. + +--- + +## Using these + +Assemble, then edit for the specific funder. Do not submit assembled blocks unedited: they +are consistent by design, which reads as boilerplate when four of them run consecutively. + +Before submitting anything containing a figure, run: + +```bash +npx tsx scripts/ops-metrics.ts +``` + +and fill every `[N — see METRICS.md]` placeholder from the fresh output. diff --git a/ops/funding/PIPELINE.md b/ops/funding/PIPELINE.md new file mode 100644 index 0000000..3fc54e6 --- /dev/null +++ b/ops/funding/PIPELINE.md @@ -0,0 +1,58 @@ +# Funding pipeline + +**Last reviewed:** 2026-08-29 + +Opportunities and their state. Reusable prose lives in +[`NARRATIVE-BLOCKS.md`](NARRATIVE-BLOCKS.md). + +**No funder contacts, no reviewer names, no rejection feedback containing individuals.** +Those go in `ops/private/`. Organizations and public deadlines only. + +**Status values:** `researching` · `drafting` · `submitted` · `awarded` · `declined` · `passed` + +## Active and recent + +| Opportunity | Status | Deadline | Amount | Notes | +|---|---|---|---|---| +| Just Tech Fellowship (SSRC) | `submitted` | — | — | Materials in `just-tech-application/` (gitignored): CV, personal statement, work proposal, logic model, form answers. Outcome not recorded here yet. | + +## To research + +Categories that fit this project, in rough order of fit. None investigated yet. + +| Category | Why it fits | What to check | +|---|---|---| +| Public health / housing-and-health funders | The project's core framing is a health one, not a consumer-tech one, and the instrument is drawn from public health research. Strongest thematic match. | Whether they fund tools rather than studies | +| Civic tech and public interest technology | Open methodology, public record, single-maintainer independent build. | Whether a solo maintainer is eligible without an institutional sponsor | +| Housing justice and tenant organizing funders | Direct alignment with tenant power and landlord accountability. | Whether they fund infrastructure or only organizing | +| Local Boston-area community foundations | Geographic specificity is an advantage here rather than a limit. | Fiscal sponsorship requirements | +| Journalism / data-transparency funders | The portfolio-pattern angle is investigative in character. | Usually requires a publishing partner | + +## The dependency worth naming + +Most funders want traction, and traction here means reviews. The +[growth strategy](../growth/STRATEGY.md) target is one landlord portfolio page with enough +reviews that the pattern is visible. + +**That page is also the strongest possible funding artifact.** It demonstrates the argument +in a way no narrative can. Sequencing funding applications after it exists is likely worth +more than applying sooner with a thinner case. + +The counter-argument: some funders support early-stage work specifically, and waiting has +its own cost. Both are true. The judgment is per-opportunity. + +## Reusable assets + +| Asset | Where | State | +|---|---|---| +| Logic model | `Org Docs/` (gitignored) | HTML + PDF, built for the Just Tech application | +| CV | `just-tech-application/` (gitignored) | Current as of that submission | +| Personal statement | `just-tech-application/` (gitignored) | Tailored to Just Tech; needs generalizing for reuse | +| Work proposal | `just-tech-application/` (gitignored) | Same | +| Narrative blocks | [`NARRATIVE-BLOCKS.md`](NARRATIVE-BLOCKS.md) | Current, checked against MASTER.md | +| Live metrics | [`../METRICS.md`](../METRICS.md) | Generated on demand | + +**Worth doing once:** generalize the personal statement and work proposal into +funder-agnostic versions and move the reusable parts into `NARRATIVE-BLOCKS.md`. Right now +they are shaped to one application, which means the next one starts closer to scratch than +it needs to. diff --git a/ops/growth/CHANNELS.md b/ops/growth/CHANNELS.md new file mode 100644 index 0000000..574824c --- /dev/null +++ b/ops/growth/CHANNELS.md @@ -0,0 +1,53 @@ +# Growth channels + +**Last reviewed:** 2026-08-29 + +One row per channel. Status is a claim about reality, not a plan. + +**Status values:** `untried` · `testing` · `working` · `failed` · `parked` + +Record failures with as much care as successes. A channel that didn't work saves the next +attempt, and it is the entry nobody writes down. + +Never name individuals here. Organizations only. Contacts and conversations go in +`ops/private/`. + +## Channels + +| Channel | Status | Last touched | What happened | +|---|---|---|---| +| Tenant unions / organizing groups | `untried` | — | Highest-potential: reaches people already angry at a landlord, already organized, already trusting the messenger. Depends on a partner willing to distribute. | +| Legal aid / housing clinics | `untried` | — | Reaches tenants at the moment of a dispute. Ethically delicate: people in active legal trouble should not be recruited carelessly. | +| Graduate and professional student housing groups | `untried` | — | High turnover, concentrated in large buildings, comfortable with surveys, motivated by the next cohort. Good depth-per-effort fit. | +| Neighborhood associations / online neighborhood groups | `untried` | — | Broad reach, low depth. Risks the breadth trap described in STRATEGY.md. | +| Instagram / organic social | `testing` | 2026-05-31 | The "we show our work" methodology angle exists and the worked-example section shipped to `/methodology` to support it. No measured recruitment result. | +| Press / local housing journalism | `untried` | — | Better suited to a moment (a finding, a portfolio page, a report) than to a cold pitch. Hold until there is something to show. | +| Direct outreach to tenants of one target landlord | `untried` | — | The depth play from STRATEGY.md. Most likely to produce the demonstration page. Also the most labor-intensive and the most sensitive. | +| Reddit and local forums | `untried` | — | Where people already complain about landlords. Community rules on self-promotion are the obstacle; participating honestly rather than posting a link is the approach. | + +## Notes on the two most promising + +**Tenant unions** are the best fit on paper: the audience is pre-qualified, the messenger +is trusted, and the project's ethics align with theirs. The risk is that a union sees a +review site as competing with organizing rather than supporting it. The pitch has to lead +with what the data does for *them*, which is documenting a landlord's pattern across a +portfolio in a form they can use. + +**Direct outreach around one landlord** is the only channel that reliably produces depth. +It is also the one that most requires care: contacting tenants of a specific landlord, by +building, is exactly the pattern that looks like harassment if done badly, and it puts +those tenants at retaliation risk if the outreach is traceable. Any version of this needs +a partner organization fronting it, not the project cold-contacting residents. + +## Instrumentation gap + +No channel above can currently be measured. The site has no analytics, deliberately, for +privacy reasons. There is no way to know whether a visitor came from Instagram, started a +review, and abandoned it at step three. + +This is a real strategic blind spot, and it is not resolved by adding analytics carelessly. +Any measurement must not compromise reviewer anonymity. Aggregate, non-identifying funnel +counts are likely compatible with the privacy commitments; per-user tracking is not. + +**Open decision:** whether to add privacy-preserving funnel instrumentation. See +STRATEGY.md open questions 1 and 2. diff --git a/ops/growth/STRATEGY.md b/ops/growth/STRATEGY.md new file mode 100644 index 0000000..16780e9 --- /dev/null +++ b/ops/growth/STRATEGY.md @@ -0,0 +1,85 @@ +# Growth strategy + +**Last reviewed:** 2026-08-29 +**Numbers:** see [`../METRICS.md`](../METRICS.md), generated from production. Don't quote figures from here. + +## The constraint, stated plainly + +RateMyPlace works. The instrument is grounded in validated research, the scoring is +published, the site is fast and secure, and moderation and disputes function. + +It does not yet have enough reviews to be useful to the person it was built for. + +Three numbers say it, all from production: + +- **53 approved reviews.** +- **40 of 91 buildings have no review at all.** A prospective tenant searching their + address most likely finds nothing. +- **2 of 27 landlords clear the three-review threshold.** The landlord portfolio view, + which is the feature no other site offers, is dark for 25 of them. + +That last one matters most. Building reviews are useful individually. Landlord patterns +are the thing that changes decisions, and patterns need volume. + +## What this means for prioritization + +**Depth beats breadth.** 53 reviews spread across 51 buildings produces almost no +aggregate signal. The same 53 concentrated on 15 buildings owned by 5 landlords would +light up five portfolio pages and demonstrate the product's actual argument. + +The implication is uncomfortable but clear: **targeted recruitment beats broad awareness.** +A campaign that brings 200 people to review 200 different buildings leaves the site as +thin as it is now. Recruiting 30 tenants from the same large landlord's buildings produces +the first real portfolio page. + +**Working principle:** pick a landlord with many units, recruit across their buildings, +and make that page the demonstration. + +## What we know about the audience + +Two distinct groups, often confused: + +**Reviewers** are current or recent tenants. They have to be motivated by something other +than self-interest, because the review helps the *next* tenant, not them. The motivations +that plausibly work: anger at a specific landlord, solidarity, and being asked directly by +someone they trust. Not: a general appeal to civic good. + +**Readers** are prospective tenants, mid-search, usually stressed and time-limited. They +arrive with a specific address. If that address is empty, they leave and don't come back. + +These need different approaches, and the reader problem is downstream of the reviewer +problem. **Do not spend effort on reader acquisition until coverage supports it.** + +## Channels + +See [`CHANNELS.md`](CHANNELS.md) for status and results of each. + +The honest state: nothing has been systematically tried. This is the work. + +## Open questions + +Things that would change the strategy and are not yet answered: + +1. **Does the verification step suppress submissions?** 15 of 53 reviews are verified, + which is a high proportion, but nobody knows how many people started a review and + abandoned it. There is no funnel instrumentation. +2. **Where do people give up in the form?** It is a five-step, 32-item survey. That is a + real ask. Unmeasured. +3. **Would a partner organization actually distribute this?** Untested. See + [`../partners/LANDSCAPE.md`](../partners/LANDSCAPE.md). +4. **Is New Haven a distraction or a second beachhead?** The enrichment adapter supports + it, and some reviews are there. Splitting a thin dataset across two cities may be + making both weak. + +Question 1 and 2 are answerable with instrumentation and would change what to do. +Currently the project has no analytics at all, deliberately, for privacy reasons. Any +measurement here has to be designed so it does not compromise reviewer anonymity, which is +a genuine constraint and not an excuse. + +## What success looks like in three months + +Not a review count. A demonstration: + +**One landlord portfolio page with enough reviews across enough buildings that the pattern +is visible and undeniable.** That page is the argument for the whole project, and it can +be shown to funders, partners, and press. Nothing else on the site does that job. diff --git a/ops/partners/LANDSCAPE.md b/ops/partners/LANDSCAPE.md new file mode 100644 index 0000000..d8e7c1a --- /dev/null +++ b/ops/partners/LANDSCAPE.md @@ -0,0 +1,106 @@ +# Partner landscape + +**Last reviewed:** 2026-08-29 + +Organization types, what each could offer, and what would make them say no. + +**Organizations only. No individuals, no contact details, no notes from conversations.** +Those belong in `ops/private/`, which is gitignored. This is a safety rule: see +[`../AGENTS.md`](../AGENTS.md). + +## Why partners matter more here than for most projects + +RateMyPlace cannot ethically do its highest-value outreach alone. + +The growth strategy says the fastest path to a useful dataset is depth: many reviews across +one landlord's buildings. But a project cold-contacting tenants building by building, +asking about their landlord, is indistinguishable from harassment and puts those tenants at +retaliation risk if the outreach is traceable. + +**A partner organization fronting that outreach solves both problems.** They have standing +with the tenants, existing trust, and a reason to be asking. This is not a nice-to-have +channel. It is the mechanism that makes the core strategy safe to execute. + +## Organization types + +### Tenant unions and organizing groups + +**Best fit.** Members are already organized around a specific landlord, already motivated, +and already trust the messenger. + +**What they'd get:** documentation of a landlord's pattern across a portfolio, in a +structured form usable in negotiation, press, and city hearings. Individual complaints are +easy to dismiss; a portfolio pattern is not. + +**Why they might say no:** a review site can read as competing with organizing, or as +extracting labor from members for someone else's platform. It may also read as a +liability, since public reviews can expose members. + +**How to address it:** lead with what the data does for their campaign, not with the +platform. Offer the aggregate view as their tool. Be explicit that anonymity is enforced +by design and that the project has a dispute process, so members are not exposed. + +### Legal aid and housing clinics + +**What they'd get:** condition documentation with a timestamp and a structured instrument +behind it, plus a way to see whether other tenants of the same landlord report the same +issues. + +**Why they might say no:** their clients are in active legal proceedings, where anything +published can be used against them. A clinic's duty to a client will always outrank a +platform's data needs, correctly. + +**How to address it:** do not recruit at the point of active litigation. The useful +relationship is probably referral after a matter closes, or clinics using the site as a +research tool rather than a submission channel. + +### Graduate and professional student housing groups + +**What they'd get:** something concretely useful to next year's cohort, which is a +recurring need these groups already try to meet with informal spreadsheets. + +**Why they might say no:** low institutional risk appetite, and universities can be +skittish about anything naming local landlords. + +**Best structural fit for depth:** high turnover, concentrated in large buildings owned by +few landlords, comfortable with surveys. + +### Housing policy and advocacy organizations + +**What they'd get:** an evidence base for policy arguments about conditions and +accountability, from tenants directly rather than from complaint records that undercount. + +**Why they might say no:** dataset is currently too small to support a policy claim. This +is the partner type most gated on the growth work. + +### Community development corporations and housing nonprofits + +**Complicated.** Many are themselves landlords. A CDC that owns and manages units has an +obvious conflict with a landlord review platform, and approaching one without recognizing +that reads as naive. + +**Where it could work:** as a quality signal for organizations confident in their own +management, or in their tenant services role rather than their ownership role. Handle +carefully. + +## What to prepare before any conversation + +1. **The demonstration page.** One landlord portfolio with a visible pattern. Nothing else + explains the project as fast. Currently does not exist: only 2 of 27 landlords clear the + display threshold. +2. **A clear answer on tenant safety.** Every one of these organizations will ask, first, + whether this exposes their people. The answer is real and worth rehearsing: anonymous + submission, fuzzy date display, no reviewer identity shown, moderation before + publication, and a formal dispute process rather than removal on demand. +3. **A clear answer on what you want.** Distribution, not endorsement, is usually the ask. + Be specific about the smallest useful version of it. +4. **Honesty about size.** These are organizations with limited time. Overstating the + dataset will be discovered, and the project's whole credibility rests on not doing that. + See the honest-state block in [`../funding/NARRATIVE-BLOCKS.md`](../funding/NARRATIVE-BLOCKS.md). + +## Status + +**Nothing attempted yet.** No organization in any category has been contacted. + +When that changes, record the organization type and outcome here. Record the specifics, +the people, and the conversations in `ops/private/`. diff --git a/ops/private/README.md b/ops/private/README.md new file mode 100644 index 0000000..f043dcb --- /dev/null +++ b/ops/private/README.md @@ -0,0 +1,40 @@ +# ops/private/ — not tracked + +Everything in this directory is gitignored except this README. + +## What belongs here + +- Names, roles, and contact details of people at partner organizations +- Notes from conversations, calls, and meetings +- Anything said off the record, or said in confidence +- Funder feedback, especially on declined applications +- Anything identifying an individual reviewer + +## What does not + +Strategy, organization-level landscape, reusable narrative, approved messaging, and +metrics all live in the tracked files one level up. Keeping them tracked is what lets +agents read them and what makes them survive a laptop failure. + +## Why this split exists + +RateMyPlace's first non-negotiable is that people who engage with it should not face +retaliation. That was written about reviewers, but it covers the tenant organizers, legal +aid staff, and city contacts who help the project too. A committed file listing who spoke +to you and what they said is precisely the kind of artifact this project exists to argue +against, and on a public repository it would be permanent and searchable. + +The gitignore rule is a backstop. The actual rule is the one in +[`../AGENTS.md`](../AGENTS.md): **never write a named individual into a tracked file.** + +## If the repository becomes private + +This directory stays gitignored anyway. Repository visibility can change, GitHub accounts +get compromised, and a repository can be shared with a collaborator in a hurry. A file +that was never committed cannot leak in any of those cases. + +## Backups + +Because nothing here is in git, nothing here is backed up by git. If this material matters, +keep it somewhere that syncs. The repository already lives in a Google Drive folder, which +covers it, but that is worth knowing rather than assuming. diff --git a/package.json b/package.json index ebea950..3f658a4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "db:migrate:local": "npx tsx scripts/db-migrate.ts", "db:fresh": "npx tsx scripts/db-fresh.ts", "db:seed": "npx tsx scripts/db-seed.ts", - "db:setup": "npm run db:fresh && npm run db:seed" + "db:setup": "npm run db:fresh && npm run db:seed", + "ops:metrics": "tsx scripts/ops-metrics.ts" }, "dependencies": { "@astrojs/check": "^0.9.6", diff --git a/scripts/ops-metrics.ts b/scripts/ops-metrics.ts new file mode 100644 index 0000000..f968aec --- /dev/null +++ b/scripts/ops-metrics.ts @@ -0,0 +1,152 @@ +/** + * Regenerates ops/METRICS.md from the production database. + * + * Why this exists: every stale doc in this repo went stale because a human had to + * remember to update a number. These numbers update themselves. + * + * Usage: + * npx tsx scripts/ops-metrics.ts # writes ops/METRICS.md + * npx tsx scripts/ops-metrics.ts --dry-run # prints, writes nothing + * + * Requires CLOUDFLARE_API_TOKEN in the environment (see AGENTS.md traps). + * Read-only: every query is a SELECT. It never writes to the database. + */ + +import { execFileSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const OUT = resolve(ROOT, 'ops/METRICS.md'); +const DB = 'ratemyplace-db'; + +/** The review threshold below which a named party's aggregate score is withheld. */ +const NAMED_PARTY_MIN_REVIEWS = 3; + +// Invoke wrangler's JS entry point with the current node binary rather than the +// `npx` / `wrangler.cmd` shims. Node 24 refuses to spawn .cmd files without +// shell:true, and shell:true would force SQL through shell quoting. This path +// avoids both problems and behaves the same on Windows, macOS, and Linux. +const WRANGLER = resolve(ROOT, 'node_modules/wrangler/bin/wrangler.js'); + +function query>(sql: string): T[] { + const raw = execFileSync( + process.execPath, + [WRANGLER, 'd1', 'execute', DB, '--remote', '--json', '--command', sql], + { encoding: 'utf8', cwd: ROOT, maxBuffer: 32 * 1024 * 1024 }, + ); + // wrangler prefixes human-readable banner lines before the JSON payload. + const start = raw.indexOf('['); + if (start === -1) throw new Error(`No JSON in wrangler output:\n${raw.slice(0, 500)}`); + const parsed = JSON.parse(raw.slice(start)); + return parsed[0]?.results ?? []; +} + +function num(rows: Record[], key: string): number { + return Number(rows[0]?.[key] ?? 0); +} + +const counts = query(`SELECT + (SELECT COUNT(*) FROM reviews WHERE status='approved') AS reviews_approved, + (SELECT COUNT(*) FROM reviews WHERE status='pending') AS reviews_pending, + (SELECT COUNT(*) FROM buildings) AS buildings, + (SELECT COUNT(*) FROM landlords) AS landlords, + (SELECT COUNT(*) FROM property_managers) AS managers, + (SELECT COUNT(*) FROM reviews WHERE is_verified=1) AS reviews_verified, + (SELECT COUNT(*) FROM disputes) AS disputes`); + +// Landlord coverage: how many clear the named-party display threshold. +const landlordDist = query<{ n_reviews: number; n_landlords: number }>(` + SELECT n_reviews, COUNT(*) AS n_landlords FROM ( + SELECT b.landlord_id, COUNT(r.id) AS n_reviews + FROM reviews r JOIN buildings b ON b.id = r.building_id + WHERE r.status='approved' AND b.landlord_id IS NOT NULL + GROUP BY b.landlord_id + ) GROUP BY n_reviews ORDER BY n_reviews`); + +// Buildings carrying at least one approved review — the coverage that makes search useful. +const covered = query(` + SELECT COUNT(DISTINCT building_id) AS n FROM reviews WHERE status='approved'`); + +const withScore = landlordDist + .filter((r) => r.n_reviews >= NAMED_PARTY_MIN_REVIEWS) + .reduce((s, r) => s + Number(r.n_landlords), 0); +const belowScore = landlordDist + .filter((r) => r.n_reviews < NAMED_PARTY_MIN_REVIEWS) + .reduce((s, r) => s + Number(r.n_landlords), 0); + +const reviewsApproved = num(counts, 'reviews_approved'); +const buildings = num(counts, 'buildings'); +const buildingsCovered = num(covered, 'n'); + +const today = new Date().toISOString().slice(0, 10); + +const distRows = landlordDist + .map((r) => { + const flag = r.n_reviews >= NAMED_PARTY_MIN_REVIEWS ? 'shown' : 'withheld'; + return `| ${r.n_reviews} | ${r.n_landlords} | ${flag} |`; + }) + .join('\n'); + +const md = `# Metrics + +**Generated ${today} from the production database.** Do not edit by hand — run: + +\`\`\`bash +npx tsx scripts/ops-metrics.ts +\`\`\` + +Read-only. Every query is a SELECT. + +## The number that matters + +**${reviewsApproved} approved reviews.** + +Everything else in [growth/](growth/STRATEGY.md) exists to move this. The site is +methodologically sound and technically healthy; it is thin on evidence. + +## Coverage + +| Measure | Count | +|---|---| +| Approved reviews | ${reviewsApproved} | +| Pending review queue | ${num(counts, 'reviews_pending')} | +| Verified reviews (proof of address) | ${num(counts, 'reviews_verified')} | +| Buildings in database | ${buildings} | +| Buildings with at least one review | ${buildingsCovered} | +| Buildings with no review yet | ${buildings - buildingsCovered} | +| Landlords | ${num(counts, 'landlords')} | +| Property managers | ${num(counts, 'managers')} | +| Disputes filed | ${num(counts, 'disputes')} | + +${buildingsCovered} of ${buildings} buildings carry a review. A search that returns an +empty building is a visitor who leaves. + +## Landlord score coverage + +A landlord aggregate is withheld below ${NAMED_PARTY_MIN_REVIEWS} approved reviews, so a +named party is never scored on thin evidence. + +| Approved reviews | Landlords | Score | +|---|---|---| +${distRows} + +**${withScore} landlord${withScore === 1 ? '' : 's'} currently show a score. ${belowScore} are withheld.** + +This is the clearest statement of the constraint. The threshold is a deliberate fairness +choice, not a bug — but it means landlord pages only become useful with volume. + +--- + +*Regenerated by \`scripts/ops-metrics.ts\`. If this file's date is old, the numbers are +old — rerun it before quoting any figure in a grant application, a pitch, or a post.* +`; + +if (process.argv.includes('--dry-run')) { + console.log(md); +} else { + writeFileSync(OUT, md, 'utf8'); + console.log(`Wrote ${OUT}`); + console.log(`${reviewsApproved} approved reviews | ${withScore} landlords scored, ${belowScore} withheld`); +}