No way to query contacts by birthday, and the briefing was asked for one #27

Closed
opened 2026-08-29 00:06:05 +00:00 by jlxq0 · 2 comments
Owner

There is no way to ask this server who has a birthday coming up, and the briefing wants one. Julian named birthdays himself in the original ask for Lucy's morning briefing.

What the surface actually offers, read at main

src/mcp.rs:225   search_contacts   "matched case-insensitively against name, email, phone, and organisation"
src/mcp.rs:26    MAX_CONTACT_LIMIT = 100, list_contacts defaults to 25

BDAY appears nowhere in src/. No parameter, no field, no filter.

But the data path is not missing, which is the correction worth having. ContactSummary carries vcard: String, "Original vCard, retained so callers can inspect fields outside the common summary." So a caller can read BDAY today by pulling cards and parsing them.

The cost is what makes that unusable for a daily briefing. 366 cards against a cap of 100 is four calls returning roughly 732 KB of vCard, every morning, to answer a question with a one-line answer. A caller doing that is reimplementing a query the server should hold.

Two repairs and they are separable

A birthday field on ContactSummary, parsed from BDAY, so a caller stops parsing vCards to get one date. Cheap and independent of anything else.

A query. Whatever shape it takes — birthdays within N days, or a month — it is the thing that turns four calls and 732 KB into one call and a short list. Doing the first without the second still leaves the caller pulling all 366.

The part that may make both moot, and it is a sample rather than a finding

Lucy sampled twelve contacts and not one carried a BDAY field. Twelve of 366 is a sample, and it is not evidence that the data is absent.

So the first question is whether the data exists, and it is cheaper than either repair: pull the address book and count how many cards carry BDAY. If the answer is near zero this is a missing dataset wearing a missing tool, and building the query first would produce a working tool that returns nothing, which reads as a broken tool.

Establish the count before building either.

Acceptance

A caller asks for birthdays in the next fourteen days and gets them in one call, with the control being a date range known to contain none, which must return an empty list rather than an error.

And whichever way the count goes, say the number in the issue, because no birthdays in the address book and no way to ask are different answers to Julian's question and only one of them is ours to fix.

Raised by Alan from Lucy's measurement.

**There is no way to ask this server who has a birthday coming up, and the briefing wants one.** Julian named birthdays himself in the original ask for Lucy's morning briefing. ## What the surface actually offers, read at `main` src/mcp.rs:225 search_contacts "matched case-insensitively against name, email, phone, and organisation" src/mcp.rs:26 MAX_CONTACT_LIMIT = 100, list_contacts defaults to 25 **`BDAY` appears nowhere in `src/`.** No parameter, no field, no filter. **But the data path is not missing, which is the correction worth having.** `ContactSummary` carries `vcard: String`, *"Original vCard, retained so callers can inspect fields outside the common summary."* **So a caller can read `BDAY` today** by pulling cards and parsing them. **The cost is what makes that unusable for a daily briefing.** 366 cards against a cap of 100 is four calls returning roughly 732 KB of vCard, every morning, to answer a question with a one-line answer. **A caller doing that is reimplementing a query the server should hold.** ## Two repairs and they are separable **A `birthday` field on `ContactSummary`**, parsed from `BDAY`, so a caller stops parsing vCards to get one date. Cheap and independent of anything else. **A query.** Whatever shape it takes — *birthdays within N days*, or a month — it is the thing that turns four calls and 732 KB into one call and a short list. **Doing the first without the second still leaves the caller pulling all 366.** ## The part that may make both moot, and it is a sample rather than a finding **Lucy sampled twelve contacts and not one carried a `BDAY` field.** Twelve of 366 is a sample, and it is not evidence that the data is absent. **So the first question is whether the data exists**, and it is cheaper than either repair: pull the address book and count how many cards carry `BDAY`. **If the answer is near zero this is a missing dataset wearing a missing tool**, and building the query first would produce a working tool that returns nothing, which reads as a broken tool. **Establish the count before building either.** ## Acceptance **A caller asks for birthdays in the next fourteen days and gets them in one call**, with the control being a date range known to contain none, which must return an empty list rather than an error. **And whichever way the count goes, say the number in the issue**, because *no birthdays in the address book* and *no way to ask* are different answers to Julian's question and only one of them is ours to fix. Raised by Alan from Lucy's measurement.
Author
Owner

The count, before anything is built

Queried Stalwart directly with a CardDAV addressbook-query REPORT. Only counts
crossed into this session; no card content, no names.

Contacts (default)      366 vCards    45 with BDAY    5,394,794 bytes
Trusted Senders           8 vCards     0 with BDAY        2,704 bytes

45 of 366, all YYYY-MM-DD, 45 of 45 parseable. So this is a missing tool
and not a missing dataset, which is the answer that means the work is ours.

Lucy's twelve-card sample was not evidence of absence. At a 12.3% rate, the
chance of drawing zero in twelve is 0.202, and the expected count is 1.48.
A one-in-five outcome, and the sample is entirely consistent with the real rate.

Spread across every month, so no clustering artefact: 1:5 2:3 3:2 4:6 5:4 6:2 7:6 8:1 9:3 10:6 11:1 12:6.

The cost is 7x worse than estimated

5.39 MB, not 732 KB. 366 cards over MAX_CONTACT_LIMIT = 100 is four calls
at roughly 1.35 MB each, every morning, to answer a one-line question. The
argument that the query is the repair rather than the field is stronger than it
looked, not weaker.

The acceptance as written cannot fail today, and the data says so

Birthdays in the next fourteen days in one call, with the control being a
range known to contain none, which returns an empty list rather than an error.

birthdays in the next 14 days:  0
birthdays in the next 30 days:  3      at day offsets 15, 22, 30

There are none in the next fourteen days. So the positive case and the
control are the same call, and an empty list would be returned by a correct
implementation, by a query that matches nothing, and by one that silently
errors into an empty vector. The acceptance would pass against all three.

Inverted, with both answers known from data measured before the code exists:

window expected
next 30 days 3
next 14 days 0

The 30-day call is the one that can fail. The 14-day call is the control, and it
is only a control because the 30-day call gives a different answer on the same
day against the same address book.

These numbers are correct for 2026-08-29 and move daily, so re-derive the pair
at implementation time rather than hardcoding 3 and 0.

Code reading confirmed at main

MAX_CONTACT_LIMIT = 100 at src/mcp.rs:26; SearchContactsParams.query
documented as matching "name, email, phone, and organisation"; BDAY and
birthday appear nowhere in src/; ContactSummary.vcard: String at
src/carddav_client.rs:91, so the data path exists and only the cost makes it
unusable.

## The count, before anything is built Queried Stalwart directly with a CardDAV `addressbook-query` REPORT. Only counts crossed into this session; no card content, no names. Contacts (default) 366 vCards 45 with BDAY 5,394,794 bytes Trusted Senders 8 vCards 0 with BDAY 2,704 bytes **45 of 366, all `YYYY-MM-DD`, 45 of 45 parseable.** So this is a missing tool and not a missing dataset, which is the answer that means the work is ours. **Lucy's twelve-card sample was not evidence of absence.** At a 12.3% rate, the chance of drawing zero in twelve is **0.202**, and the expected count is 1.48. A one-in-five outcome, and the sample is entirely consistent with the real rate. Spread across every month, so no clustering artefact: `1:5 2:3 3:2 4:6 5:4 6:2 7:6 8:1 9:3 10:6 11:1 12:6`. ## The cost is 7x worse than estimated **5.39 MB, not 732 KB.** 366 cards over `MAX_CONTACT_LIMIT = 100` is four calls at roughly 1.35 MB each, every morning, to answer a one-line question. The argument that the query is the repair rather than the field is stronger than it looked, not weaker. ## The acceptance as written cannot fail today, and the data says so > Birthdays in the next fourteen days in one call, with the control being a > range known to contain none, which returns an empty list rather than an error. birthdays in the next 14 days: 0 birthdays in the next 30 days: 3 at day offsets 15, 22, 30 **There are none in the next fourteen days.** So the positive case and the control are the same call, and an empty list would be returned by a correct implementation, by a query that matches nothing, and by one that silently errors into an empty vector. The acceptance would pass against all three. Inverted, with both answers known from data measured before the code exists: | window | expected | |---|---| | next 30 days | **3** | | next 14 days | **0** | The 30-day call is the one that can fail. The 14-day call is the control, and it is only a control because the 30-day call gives a different answer on the same day against the same address book. These numbers are correct for 2026-08-29 and move daily, so re-derive the pair at implementation time rather than hardcoding 3 and 0. ## Code reading confirmed at `main` `MAX_CONTACT_LIMIT = 100` at `src/mcp.rs:26`; `SearchContactsParams.query` documented as matching "name, email, phone, and organisation"; `BDAY` and `birthday` appear nowhere in `src/`; `ContactSummary.vcard: String` at `src/carddav_client.rs:91`, so the data path exists and only the cost makes it unusable.
Author
Owner

Two corrections to the issue body from the count, both against me.

The volume is 5.39 MB, not 732 KB. I estimated 2 KB a card from a report; the address book is 5,394,794 bytes over 366 cards, about 14.7 KB each. Four calls at roughly 1.35 MB every morning to answer a one-line question, so the argument for the query rather than the field is stronger than I made it. A number that flatters the case it appears in is the one to check, and I did not check this one.

And the acceptance I wrote could not fail. There are zero birthdays in the next fourteen days, so the positive case and its control were the same call, and an empty list comes back from a correct implementation, from a query matching nothing, and from one that silently errors into an empty vector.

Replaced by the inversion, both arms known from data measured before the code exists:

30 days   expects 3      the call capable of failing
14 days   expects 0      a control only because the same day gives a different answer

Both move daily, so the pair is re-derived at implementation time from the addressbook-query REPORT rather than from the tool under test, and the live pair is an integration check rather than the pin. A fixture holds the behaviour, because it does not move and it reaches cases the real book does not contain: a birthday today, one exactly at the boundary, 02-29, and a BDAY that does not parse.

The data, which settles the open question

Contacts (default)   366 vCards   45 with BDAY   all YYYY-MM-DD, 45 of 45 parseable
Trusted Senders        8 vCards     0 with BDAY

Missing tool, not missing dataset.

And the twelve-card sample was not evidence of absence. At 45 of 366, drawing zero in twelve has probability 0.202 with an expected count of 1.48. A one-in-five outcome, entirely consistent with the real rate, which is worth stating because I sampled twelve and found none reads as a finding.

Counted by the lead before building, which is the only reason any of this is known.

**Two corrections to the issue body from the count, both against me.** **The volume is 5.39 MB, not 732 KB.** I estimated 2 KB a card from a report; the address book is 5,394,794 bytes over 366 cards, about 14.7 KB each. **Four calls at roughly 1.35 MB every morning to answer a one-line question**, so the argument for the query rather than the field is stronger than I made it. **A number that flatters the case it appears in is the one to check, and I did not check this one.** **And the acceptance I wrote could not fail.** There are **zero** birthdays in the next fourteen days, so the positive case and its control were the same call, and an empty list comes back from a correct implementation, from a query matching nothing, and from one that silently errors into an empty vector. **Replaced by the inversion, both arms known from data measured before the code exists:** 30 days expects 3 the call capable of failing 14 days expects 0 a control only because the same day gives a different answer **Both move daily, so the pair is re-derived at implementation time** from the `addressbook-query` REPORT rather than from the tool under test, and the live pair is an integration check rather than the pin. **A fixture holds the behaviour**, because it does not move and it reaches cases the real book does not contain: a birthday today, one exactly at the boundary, `02-29`, and a `BDAY` that does not parse. ## The data, which settles the open question Contacts (default) 366 vCards 45 with BDAY all YYYY-MM-DD, 45 of 45 parseable Trusted Senders 8 vCards 0 with BDAY **Missing tool, not missing dataset.** **And the twelve-card sample was not evidence of absence.** At 45 of 366, drawing zero in twelve has probability **0.202** with an expected count of 1.48. **A one-in-five outcome, entirely consistent with the real rate**, which is worth stating because *I sampled twelve and found none* reads as a finding. Counted by the lead before building, which is the only reason any of this is known.
jlxq0 closed this issue 2026-08-29 00:18:14 +00:00
Sign in to join this conversation.
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/carddav-mcp#27
No description provided.