Skip to content

Milestone 1: lock the scope, and name what is not in it #82

Description

@davidtaing

TL;DR

  • The problem — milestone 1 needs its boundary written down in one place, so that adding something is a deliberate act rather than a discovery halfway through a branch.
  • The fix — this issue is that list: a scope tree of what is in, what is deliberately out, and the delivery order. 18–23.5 engineering days, roughly five to six weeks. It owns the boundary only — docs/scope.md owns cost, the spec owns the model, CONTEXT.md owns the vocabulary, and where they disagree with this issue they win.
  • To decide — where an enquiry lands. docs/scope.md and the spec contradict each other, and Wire up the contact form #2 cannot be built until it is settled.
Full context — the reasoning, the constraints and what has already been ruled out. This is the part a coding agent should read.

Milestone 1 is the smallest thing that is a product: a visitor lands, finds a practitioner, and can contact them; a practitioner signs themselves up and Bluehex vouches for their credentials. This issue is the list. Anything not on it is not committed to milestone 1, and adding something here is a deliberate act rather than a discovery halfway through a branch.

This issue owns the boundary, not the cost or the design. docs/scope.md owns the estimates and the delivery order, docs/spec/profile-and-credentials.md owns the model and the DDL, CONTEXT.md owns the vocabulary, docs/adr/ owns the decisions. Where this issue and any of those disagree, they win and this issue is the bug.

The scope tree

Grouped by what the thing is rather than when it lands, so it survives the milestone and can carry the roadmap forward. The delivery order is further down and is a different question. This tree replaces the flat list that stood here — one list, so there is nothing to keep in sync.

Bluehex
│
├── Site
│   ├── Landing page and directory shell ..................... shipped
│   ├── Directory reads real data ............................ #53
│   └── Contact
│       ├── /contact page, mailto stopgap .................... shipped
│       └── The enquiry actually sends ....................... #2      ← destination unsettled
│
├── Identity
│   ├── Admin authority: role, admins, access token hook ..... shipped, #47
│   ├── The hosted project's copy of the hook ................ #41
│   └── Accounts: sign up, sign in, sessions, protection ..... #83
│
├── Profiles
│   ├── Schema
│   │   ├── Core: practitioners, contacts, review notes ...... #49
│   │   ├── Credentials, verification, badge clearing ........ #50
│   │   ├── Credentials come from a fixed list .............. folded into #49, #50
│   │   └── Withdrawal, account deletion, erasure ............ #52
│   ├── What the practitioner does
│   │   ├── Submission, and the verification step ............ #14
│   │   ├── Profile editor .................................. #71
│   │   ├── Scheduling URL, as a button ..................... #85
│   │   └── Portfolio links: website, GitHub, LinkedIn ...... #84
│   └── What Bluehex does
│       └── Review queue, and approval ...................... #72
│
├── Assurance
│   ├── The schema's invariants have tests .................. #45
│   └── The #35 proof transcript, re-expressed as tests ..... #55
│
├── Delivery
│   └── Migrations pushed to the hosted project from CI ..... #48
│
└── Meetup
    └── The next event, on the home page .................... #60

Where an enquiry lands is contradicted across two documents, and #2 cannot be built until it is settled. docs/scope.md says the enquiry reaches Bluehex, closed by elimination on 2026-08-18. docs/spec/profile-and-credentials.md says the enquiry form "remains the only route to a practitioner's inbox", and src/app/contact/contact-form.tsx records the spec as superseded on 2026-08-16 in favour of delivering to contact_email — which is why it says #2 needs a server-side send rather than a hosted form service.

The scope document's elimination argument does not hold: what is out of scope is publishing the address, and a server-side send to contact_email publishes nothing. By the precedence rule the spec wins and docs/scope.md is the bug, but this needs deciding rather than inferring, because it decides #2's implementation. Either way a published address stays out, and visitors who want the practitioner directly use the scheduling and portfolio links.

Outside the tree

Everything below is real work that is not in milestone 1. It is listed against the same branches so that a later milestone extends this tree rather than starting a new one:

├── Site
│   └── Contact
│       └── Proxied enquiry address, copying both parties ... milestone 2 candidate
├── Profiles
│   └── Service catalogue, and promoting into it ............ #78
└── Assurance
    ├── Vitest .............................................. #5
    └── React Testing Library ............................... #56

(unbranched) Automated dependency updates ................... #12

#9 looks closeable — the domain model it asked for is written and settled across docs/spec/profile-and-credentials.md, CONTEXT.md and ADR-0001.

Four things are out on purpose rather than unscheduled, and each is close enough to something in the tree to be inferred from it. They are the next section.

Gaps, and what closed them

Three pieces were committed with nowhere to be tracked. Two are now cut and one turned out to be already done:

Explicitly out of scope

Each is out on purpose, and each is close enough to something in scope to be inferred from it:

  • Cross-verification against Skilljar, LinkedIn or this site. The Skilljar half is closed, not merely unscheduled — the API is tenant-scoped to Anthropic. The rest is out because the badge means a human looked, and an automated match either adds nothing to that or quietly replaces it.
  • A full bookings calendar. A scheduling URL is a string that leaves for a tool the practitioner already runs. The moment Bluehex holds availability, or a booking exists as a row here, that is the line.
  • A Meetup events page. The card is in, the page is out, and Meetup's own page is the link. Moved out deliberately after discussion.
  • A practitioner's email address on the profile. Out rather than deferred, and it survives the reversal that let every other kind of link through. practitioner_contacts has no anon grants by any route.

Delivery order

The phases are a dependency chain, not a preference. Profile reads need no auth and can come first, which puts the directory on real data early. Profile writes cannot precede auth: profiles carry an account foreign key that does not exist until the user table does, and building writes first means rewriting every mutation and every policy afterwards.

  1. Curated intake — profile arrives by mail or pull request, Bluehex checks the credentials, commits it. No database, no accounts, no secrets. 1–1.5d.
  2. 2a — Schema and the read pathProfile core: contacts, practitioners, review notes, policies, guard and admin RPCs #49, practitioner_credentials: per-credential verification and badge clearing #50, Give the schema's invariants somewhere to be tested #45, Wire up the hosted Supabase project and the deployed environment #41, Push migrations to the hosted project from CI #48, The directory reads from the database #53, Add the scheduling URL to the profile, as a button #85, Carry the portfolio links into the app: website, GitHub and LinkedIn #84. No auth in the application. 3–4.5d.
  3. 2b — AuthenticationAuthentication: sign up, sign in, sessions, and route protection #83. 3–4d.
  4. 2c — Profile writesBuild profile submission and the Bluehex verification step #14, Build the profile editor from the prototype, against fixtures #71, Build the admin review queue from the prototype, against fixtures #72, Withdrawal, account deletion, and erasure #52. Profile CRUD, avatar upload, validation, claiming, approval queue, admin dashboard. 8–10d.

The Meetup card (#60) and the contact form (#2) are independent of the chain and can land at any point.

Total: 18–23.5 engineering days, roughly five to six weeks elapsed at the 4 engineering-days-a-week capacity assumption. Read it as the sum of the sections in docs/scope.md rather than as an independent estimate — if one of them moves, this moves with it.

The number that moves this most is attention, not typing speed. Work labelled afk compresses hard because it is close to transcription from a settled design; work labelled hitl barely compresses at all, and 2b and 2c are mostly hitl.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationhitlNeeds a human (decision/design/review)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions