Skip to content

chore(memory): promote strong-fit contacts drafts for review - #132

Open
Hareet wants to merge 22 commits into
mainfrom
memory/promote-contacts
Open

chore(memory): promote strong-fit contacts drafts for review#132
Hareet wants to merge 22 commits into
mainfrom
memory/promote-contacts

Conversation

@Hareet

@Hareet Hareet commented Jun 24, 2026

Copy link
Copy Markdown
Member

Promotes 44 strong-fit contacts drafts from agent-memory/_pending/ into agent-memory/domains/contacts/issues/ for squad content review.

Categories: feature (27), bug (10), improvement (7)
Themes: cht-datasource person/place create/update/get, contact-summary fetch performance, contact forms/attachments, lineage handling.

All 44 carry domainFit: strong + a ## Domain Rationale section. 7 weak-fit drafts deferred for later.

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes, though to be clear the fix is upstream and not something to patch by hand here. This seeder faithfully promotes what the distillation pipeline generated, so nothing is wrong with the promotion itself. The problem is in the drafts the pipeline produced, and I verified it against the live medic/cht-core API for every draft in this PR.

The defect: issueNumber / issueUrl name the merge PR, not the resolved issue

One precise note up front, because the intuitive version of this is not what is happening: issueNumber and issueUrl always agree with each other (the URL is literally /issues/<issueNumber>). The bug is that in 31 of 44 drafts here, that shared number is a pull request, so /issues/N silently redirects to /pull/N. The real resolved issue survives only in the PR-title slug. For example:

  • 10043-feat10036-add-personqualifier... stores issueNumber: 10043 / issueUrl: .../issues/10043, but 10043 is the merge PR ("feat(#10036): add PersonQualifier and related functions"). The real issue is #10036 (closed): "To have API that can create people".
  • 10057-feat10038-add-placequalifier... stores issueNumber: 10057 / issueUrl: .../issues/10057, but 10057 is the merge PR ("feat(#10038): add PlaceQualifier and related methods"). The real issue is #10038 (closed): "To have API that can create places".

This is the same scraper behaviour flagged on #121, and it is pipeline-wide: across the four clean seeders, 60 of 107 drafts are affected.

Duplicates in this PR. Because each draft is keyed by its PR, some resolved issues appear more than once:

  • Several issues map to many drafts: #10038 (5 drafts), #10036 (3), #10037 (2), #8985 (2), #9241 (2).
  • Three are also in the authentication seeder #131 (cross-domain): #9835, #9065, #6543.

Also. One draft (9311) is a worse variant: it stores issueNumber: 9241, which is neither its own PR nor the issue its PR title references, but a third, unrelated issue.

Why request-changes rather than merge-and-fix-later

This corpus is the agent's memory of resolved issues (consumed by the Context Analysis Agent, see #135). A reference that resolves to a PR instead of the issue is simply wrong data, and it specifically defeats #135's planned de-duplication by issue id, since the "id" ends up being the PR id. Regenerating the drafts after the upstream fix will rewrite most of these files anyway, so merging now would churn the corpus twice.

Suggested fix (at the source, not file by file)

  1. Fix the distiller/scraper to take the resolved issue from the type(#N): PR title (the filename slug already extracts it correctly), and keep the PR number in source_pr where it belongs.
  2. Regenerate and re-promote this domain's drafts.
  3. Add de-duplication by real issue id (also a #135 acceptance item).

Open to discussing the approach. Once the drafts carry the real issue references, this should be a quick re-review.

Hareet and others added 3 commits July 16, 2026 17:37
…ly, contacts)

Deterministic relink via #129's relink-issues tool: drafts whose identity
keys recorded the merge PR now point at the resolved issue. Frontmatter
id/issueNumber/issueUrl lines only; bodies untouched. 31 files relinked,
1 flagged (9311 — resolved in the follow-up review commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per sugat009's review on #132: collapse 10 duplicate clusters (10036,
10038, 10037, 8985, 9065, 9241, 9835, 9264, 9426, 9601) to one memory
per issue, folding each PR's distinct content into the canonical with a
source_prs[] provenance array (schema.json gains the optional source_prs
definition, byte-identical to PR #138's). 15 collapsed files removed.

Suspect 9311 verified against cht-core: its PR body explicitly closes
issue #9241 ("Create API endpoint for getting people"), so the stored
key was correct; folded into the 9295 canonical as a second source PR.

Cross-domain dedup: issue #6543 is canonically authentication (multi-
facility user permissions), so 9094 (webapp display facet) is removed
here and will be folded into the authentication memory on #131.

Also: scrubbed classifier/reviewer process narrative from prose,
backfilled related_issues for the 9193/9237-9242 datasource family,
fixed the #9241 title drift. All 43 PR-to-issue mappings verified
against the live cht-core API (0 mismatches); validate-schema 92/92.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Hareet
Hareet force-pushed the memory/promote-contacts branch from 3e30890 to d536424 Compare July 17, 2026 00:03
@Hareet

Hareet commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@sugat009
Rebased onto main and addressed the review: identity keys relinked via #129's tool (31 files, all 43 PR→issue mappings re-verified against the cht-core API); duplicate clusters collapsed to one memory per issue with source_prs[] (schema gains the optional field, identical to #138's definition); cross-domain dupes resolved — #9835/#9065 stay here, #6543 moves to the authentication seeder. One correction from verification: 9311 was actually keyed correctly — PR #9311's body explicitly closes #9241 (the people REST-endpoint ticket); it's now folded into the #9241 canonical alongside #9295.

@Hareet
Hareet requested a review from sugat009 July 17, 2026 00:05
Hareet added a commit that referenced this pull request Jul 17, 2026
…ation)

Per sugat009's review on #131: collapse the #8868 backport pair
(8924+8933) into one memory with source_prs[]; drop 8843 (feat(na),
closes no tracked issue — #136 skip-and-flag policy).

Cross-domain dedup: fold the webapp display facet (PR #9094, moved from
the contacts seeder) into the #6543 canonical here (source_prs 9094 +
9126); drop 9204/9205/10222 whose issues (#9203/#9065/#9835) are
canonically owned by the contacts corpus — their PR refs get recorded
there in a follow-up commit on #132.

Suspect 9955 verified against cht-core: PR body explicitly closes #9735
(the SSO epic), so the stored key was already correct; the filename
token 9760 is a stale title scope (issue #9760 is owned by 9800's file).

Also: backfill related_issues across the SSO issue family (epic 9735 +
sub-issues), scrub reviewer/process narrative from 19 files, add the
optional source_prs schema definition (identical to #138 and #132).
All 39 PR-to-issue mappings verified against the live cht-core API
(0 mismatches); validate-schema 98/98; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts)

Companion to the authentication seeder (#131) cross-domain dedup: the
auth branch drops its drafts for issues this corpus canonically owns,
so their PR provenance is recorded here — #10222 (permission checks)
on the #9835 memory, #9205 (offline-user endpoint gating) on the #9065
memory, and #9204 (admin-app facility_id backward compat) on the #9203
memory.

Also: scrub remaining reviewer/process narrative and classifier seed
references from 19 files (content unchanged, attribution and review
chronology removed), and quote the #9065 memory's source_prs entries
for YAML consistency. validate-schema 92/92; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hareet added a commit that referenced this pull request Jul 17, 2026
Per sugat009's review on #120: collapse the 3 duplicate clusters to one
memory per issue with source_prs[] — backport pairs 10068 (10073+10082)
and 10225 (10230+10243), and the 10802 sibling fixes (10803+10811)
folded into the existing issue-keyed memory; 9559 likewise folded into
the existing 9467 RapidPro memory. 5 collapsed files removed.

Corpus fix for 10729: grounded the memory in the merged PR #10730
(source_prs added; Solution/Testing now attribute the shipped fix).

Domain fit: 8717 (conversation-UI navigation to the contact page)
honestly re-annotated domainFit: weak; 10853/10477 verified as genuine
pipeline code (transitions, message-utils) and stay strong.

Also: backfill related_issues (10442->10446 closing ref, 10729<->10802,
10802->10428), scrub reviewer/process narrative from 8 files, add the
optional source_prs schema definition (identical to #138/#132/#131).
All 17 PR-to-issue mappings verified against the live cht-core API
(0 mismatches); validate-schema 76/76; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Companion to the forms (#122) and tasks (#123) seeders' cross-domain
dedup — this corpus canonically owns their issues, so the PR provenance
is recorded here: the #9835 memory gains the report-side PRs (#10022
ReportQualifier groundwork, #10246 reported_date fix), and the #10344
memory gains #10432 (targets-by-contact-id datasource support).

The 10570 draft (#10509, attachments in contact forms) is removed: the
forms corpus's curated 10509 memory owns that issue and now records
PR #10570.

validate-schema 91/91; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content re-review. Identity/dedup/schema clean; the 7 modified drafts are benign except 9915 (a Testing reword that silently drops a "Reviewer verified" attribution — minor). Remaining:

  • issue (accuracy): 9281 inverts the getAll AsyncGenerator semantics (inline).
  • issue (domain): four drafts (10043/10057/9266/9281) are pure cht-datasource Person/Place-qualifier extension → agreed data-access domain (inline on 10043). NB this is also where the 10022/10246 report-datasource content from #122 landed (in 9835) — all datasource extension currently sitting in contacts.
  • nitpick (leakage): 9007 Domain Rationale ("Per the infrastructure pitfall"); strip.

Cross-cutting (whole promote batch, not blocking this PR alone): filenames still encode the PR number while frontmatter is issue-keyed, and two draft schemas coexist (machine-distilled vs hand-authored) — worth a cleanup pass. Full detail + re-runnable Verify commands are in my consolidated review notes.

The largest cross-cutting item, moving the cht-datasource library-extension drafts to a first-class data-access domain + a weighted secondaryDomains[] field, is in the detailed data-access proposal comment on #122.

domainFit: strong
issueNumber: 9238
issueUrl: https://github.com/medic/cht-core/issues/9238
title: Add Person.v1.getAll AsyncGenerator to cht-datasource for paginated iteration over all people

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (accuracy): the draft says getAll yields pages (arrays) and shows a nested-loop snippet, but the AsyncGenerator yields individual Person docs one-by-one (PR #9281: for (const doc of docs.data) { yield doc }; the spec asserts "yields document one by one"). As written it would teach an agent the wrong iteration. Correct to e.g. for await (const person of Person.v1.getAll(ctx)(qualifier)) { … }.

---
id: cht-core-10036
category: feature
domain: contacts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (domain): 10043 (PersonQualifier), 10057 (PlaceQualifier), 9266/9281 (getPeople) are pure cht-datasource library extension (anchor PRs touch only shared-libs/cht-datasource) — the Person/Place qualifiers the agreed data-access domain (#11174 hierarchy) is organized around. They belong in data-access with secondaryDomains: [contacts]. See the detailed data-access proposal comment on #122.

Hareet added a commit that referenced this pull request Aug 5, 2026
…ne for review (#120)

* chore(memory): promote strong-fit messaging drafts for review

* fix(#135): relink id/issueNumber/issueUrl to real issues (metadata-only, messaging)

Deterministic relink via #129's relink-issues tool: drafts whose identity
keys recorded the merge PR now point at the resolved issue. Frontmatter
id/issueNumber/issueUrl lines only; bodies untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#135): collapse duplicate clusters + review fixes (messaging)

Per sugat009's review on #120: collapse the 3 duplicate clusters to one
memory per issue with source_prs[] — backport pairs 10068 (10073+10082)
and 10225 (10230+10243), and the 10802 sibling fixes (10803+10811)
folded into the existing issue-keyed memory; 9559 likewise folded into
the existing 9467 RapidPro memory. 5 collapsed files removed.

Corpus fix for 10729: grounded the memory in the merged PR #10730
(source_prs added; Solution/Testing now attribute the shipped fix).

Domain fit: 8717 (conversation-UI navigation to the contact page)
honestly re-annotated domainFit: weak; 10853/10477 verified as genuine
pipeline code (transitions, message-utils) and stay strong.

Also: backfill related_issues (10442->10446 closing ref, 10729<->10802,
10802->10428), scrub reviewer/process narrative from 8 files, add the
optional source_prs schema definition (identical to #138/#132/#131).
All 17 PR-to-issue mappings verified against the live cht-core API
(0 mismatches); validate-schema 76/76; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#136): ground messaging drafts against cht-core source

Every factual claim in the seventeen drafts on this branch was checked against
the cht-core commit it was distilled from — word-bounded `git grep`,
`diff-tree --name-status`, `ls-tree`, and reading the hunks. 189 claims
confirmed; 63 corrections applied. Each was additionally re-checked by a second
pass instructed to refute it, and five proposed corrections were discarded that
way rather than shipped.

The recurring defect is not a wrong identifier — it is a correct identifier
wrapped in a wrong story:

10073 described an inbound Express/`req.body` double-parse. The file has no
Express handler and no `req` at all; it is outbound-only. The real bug was
`sendMessage` re-parsing the response body of its own POST, which
`@medic/couch-request` had already parsed, so every send silently produced no
state change. Title, summary, problem, root cause and Code Patterns all restated
accordingly, and the e2e spec described as added was modified.

10802 used `task.status` where the field is `task.state`, and presented #10811
as a sibling guard when its commit body reads "(cherry picked from commit
6a5867b)" — a byte-identical backport of #10803. The fabricated
`isDue()`/`due_date` snippet is removed.

10497 read `resolveMany` as fanning out to several recipients when it returns
the first that resolves. 4278 and 8492 stated the opposite of the code in Design
Choices, and asserted test coverage absent from their diffs. 9364 generalised a
narrow fix. 8717 described only additions when the commit renamed a spec away.
9467 named a member that does not exist on the object at the cited line.

Also corrected across the branch: file lists that lost A/M/D status, test paths
missing the `.spec` segment, and classifier scaffolding in 10868's rationale.

Held back deliberately: five proposed corrections that did not survive
re-checking, including one whose replacement would have relocated a throw to a
line unreachable in the failing scenario. Twenty-four claims remain
unverifiable — chiefly the legacy drafts that carry no source commit and the two
whose PR numbers appear nowhere in cht-core history. Nothing was edited on the
strength of a claim that could not be checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): relink two hand-authored identities + anchors, cross-link the due_tasks pair (messaging)

- 4278 and 8492 keyed their identity to PR numbers — the round-1 defect
  class surviving in two pre-existing hand-authored drafts the relink
  tool never touched (it only reads machine frontmatter). Both prose
  bodies already named the real issues: 4278 -> #3738 (PR #4278's body:
  'Issue: #3738'), 8492 -> #8414 (PR title 'fix(#8414): sms gateway test
  flakiness'). id/issueNumber/issueUrl relinked accordingly.
- Both drafts also gain machine anchors (source_prs + the PR merge
  commit as source_sha: d88f2e256, 2c740238) so claim grounding can
  check them at their own trees instead of degrading to master-fallback
  guesses — d88f2e256's tree is where the draft's 2018-era paths are
  real, and 2c740238 touches exactly the two files the 8492 draft names.
- 10442 <-> 10802 both rework due_tasks.js state transitions but only
  10802 carried the back-reference; related_issues on 10442 now links
  cht-core-10802, making the in-batch pair symmetric (the same class
  flagged on the configuration batch's 9696/9727).

validate-schema: 76 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#136): correct two residual 10802 claims caught by the anchored probes (messaging)

Once the source_prs fallback anchored 10802 at 6a5867bb, two claims the
July-27 grounding pass missed became checkable and failed:

- 'Filter tasks by both due_date and status fields' — neither field
  exists; the real comparison is the computed due value
  (task.due || task.timestamp || doc.reported_date) plus the task.state
  guard. Same fabrication family as the review-2 inline, one bullet over.
- 'Added a sentinel integration test (due-tasks.spec.js)' — the file
  pre-existed; 6a5867bb ADDS a 69-line case to it (M, not A).

Also aligned two prose 'status' field references to 'state'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#136): correct the RapidPro broadcasts endpoint spelling (9467, messaging)

The draft named an 'api/v2/broadcast' endpoint; the service posts to
'/api/v2/broadcasts.json' (api/src/services/rapidpro.js:92 at da4b50f7).
Caught by the probes on the second anchored run — the claim only became
checkable once the API resolver anchored this hand-authored draft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#136): round-3 review, the self-contradiction class, last PR-keys (messaging)

All 16 inline items plus three contradictions review did not reach and
the follow-ups agreed as in-scope.

THE CLASS BEHIND MOST OF IT. The grounding pass corrected the sections
that assert mechanism against code and left the interpretive ones
asserting what it had just disproved, so several drafts told two stories.
A coherence pass over all 22 drafts found six; review had named three.

- 10073: Domain Rationale still located the bug inbound while Root Cause
  and Code Patterns say the file is outbound-only and never sees req.body;
  the Related Issues gloss said 'request body'; techStack still listed
  express. All now agree the double-parse was of the send RESPONSE.
- 10729: Design Choices claimed existing unit tests covered the
  functionality; the grounded Testing section says neither fix is covered.
  Bullet dropped. parseArray's mechanism narrowed to what smsparser.js
  actually does - getParser returns undefined for a non-string message or
  an unrecognized Muvuku code, not merely because def is null.
- 4278: Problem opened with 'had no test coverage', which its own Root
  Cause refutes; the illustrative fence was a composite of two real
  helpers that appears nowhere (replaced with allMessageDocs verbatim);
  the invalid-content test claims are gone (the 365-line spec's only
  'invalid|error' match is a fixture field 'errors: []').
- 10802 (not in review): one sentence said the fix landed on master and
  5.2.x AND that 5.1.x is the only line carrying it. Reworded - each patch
  reaches a different set of lines.
- 8717 (not in review): Solution credited 'navigation logic in
  sender.component.ts' while Code Patterns says it injects no Router and
  gained only two accessors; routing is the declarative routerLink.
- 3406 (not in review): Code Patterns recommended compound view keys
  'emit([task.state, when], val)' AND string keys instead of array keys.
  The PR did the latter - it changed emit([task.state, when]) to
  emit(task.state) so consumers can ask for several states in one request,
  keeping the due date in the value as sending_due_date.

ACCURACY. 10802's Root Cause blamed an eventually-consistent view; the
view is keyed ['scheduled', due] and cannot return an already-transitioned
task. Replaced with the real mechanism: the view vouches for one task
while updateScheduledTasks iterates every scheduled_task matching on due
date alone. 10802's #10754 cross-reference is deleted (it is a cookie
bug). 9467 loses the 62,000-message figure, which belongs to #10428's
empty-message workaround, and time-scopes err?.statusCode (master now
reads err?.status). 10497 no longer calls the review feedback stylistic -
it included a normalizeRecipient redesign.

DRIFT. 4278's four 2018-era paths (pre api/src, pre-wdio protractor tree)
are time-scoped in one note; the polling pattern is the durable part.

IDENTITY. The last five PR-keyed drafts are re-keyed to their real issues
- 3406->3073, 4039->3627, 4374->4110, 6995->6532, 7105->6572 - each with
source_pr/source_prs and the PR's merge commit as source_sha, closing the
class at 7 of 7. 4374's reference to the re-keyed 4278 entry now points at
#3738. Nine PRs cited in Related Issues as though they were issues are
labelled 'PR #N'. Canonical source_pr added to the five drafts that
carried only source_prs, per the schema's own wording.

Gate: validate-schema 76 passed / 0 failed; verify-drafts --online 0
blocking / 0 warnings / 0 unverified; check-coherence 0 contradictions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): keep the five hand-authored drafts out of this PR (messaging)

Reverts 3406, 4039, 4374, 6995 and 7105 to their state on main. Anchoring
them (last commit) made their claims checkable for the first time and they
do not survive it: 19 ungrounded claims across the five, including seven
fabricated metric names in 7105 (monitoring.messaging.outgoing.state and
its .delivered/.failed/.total./.seven_days/.last_hundred siblings, plus
monitoring.sentinel.backlog - none exist at its anchor), handleCallback /
RAPIDPRO_URL / RAPIDPRO_TOKEN in 6995, three files 4374 names as touched
that its backport commit never touched, and shared-libs/messaging in 4039.
8 drift hits and three contradictions sit on top of that.

These are pre-existing defects that the re-key exposed rather than caused,
but fixing them means substantially rewriting five 2017-2021 drafts - and
7105 may belong dropped rather than corrected, the way 11021 was on the
configuration branch. That is its own review, not a rider on this one, and
the reviewer had already scoped these as follow-ups outside this diff.

So this PR goes back to exactly the 17 drafts under review plus schema.json.
The re-key, the anchors, and 4374's now-stale reference to the re-keyed 4278
entry all move to a dedicated follow-up PR.

Reverting 3406 also removes a contradiction this branch had introduced: the
Code Patterns rewrite there was correct about the view (it emits msg.uuid
and task.state, no compound key) but left Design Choices still claiming the
PR emits both key shapes.

Two fixes for the 17 that stay:

- 8492: Problem blamed 'inconsistent message state setup in test factories'
  while its own Root Cause says the cause was shared mutable fixture state,
  NOT the factory failing to set a state. Same section-scoped pattern the
  reviewer identified; reworded to describe the symptom instead.
- 9467: getOutgoingMessages is real but lives in api/src/services/messaging.js,
  which the draft never said. Naming it removes a misattribution a reader
  could draw and settles a probe artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): bump 10853 lastUpdated (messaging)

Changing the stamp is itself an edit, so the freshness check needs the
final value, not the date of the content change that prompted it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): last two in-scope findings from the final gate (messaging)

- 8492: a second contradiction in the same draft. Root Cause says the bug
  was shared mutable fixture state, 'not the factory failing to set a
  state', while Design Choices credited the fix to 'proper state setup'.
  Reworded to what the fix actually buys: per-build task objects make the
  tests order-independent.
- 9467: two sentences were phrased so that a prose aside became a
  code-shaped claim probed at the wrong tree. 'current master reads
  err?.status === 400' is true of master and false at this draft's anchor,
  so a symbol-in-file probe at the anchor refutes a correct sentence; and
  quoting a whole logger.error statement cannot survive a word-bounded
  grep. Both now name the property and the call site instead of embedding
  the expression, which is also easier to read.

Both facts are unchanged and still verified: rapidpro.js:109 on master
tests status, and the logger.error call is at rapidpro.js:110 at the anchor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): three contradictions an independent audit found (messaging)

An independent verification pass found that my own round-3 sweep had
reproduced the very class it was fixing: correcting one section of a draft
and leaving its siblings asserting the disproved story.

10442 - the worst of it, and mine. Problem was rewritten to the real code
path; summary and Design Choices were not. Ground truth at 862f69a6^ is
'if (task.messages) { updatedTasks = true; utils.setTaskState(task,
'pending'); }' - a task WITH a messages array but an empty body was
promoted to pending, only a task with no messages array at all sat in
scheduled. So the summary's 'they sat in scheduled indefinitely' was false
for half the cases, and Design Choices' 'deployments keep leaving such
messages indefinitely scheduled' contradicted the Solution's own note that
leaving them in scheduled 'is itself a change'. All four sections now
describe one path, and Design Choices says what the default actually
changes rather than implying continuity.

10442 Related Issues - #10446 is 'Dont send empty messages', not 'failed/
invalid scheduled messages were not being cleared'. The gloss restated
#10428's concern. It survived the cross-reference audit because gloss and
title share the word 'messages', which is a live demonstration that one
shared content-word defeats word-disjointness in a messaging corpus.

10729 summary - 'causing fields to never match' is the pre-correction
silent-failure story, which the draft's own Problem section refutes: the
loop threw TypeError on item[0] and propagated out uncaught. Round 3 had
rewritten the second half of that sentence and left the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): three narrative corrections from the independent audit (messaging)

- 9467: 'the pre-existing logger.error call that was moved above the new
  branch' - nothing moved the log. The diff shows the '// ignore error,
  sending the message will be retried later' COMMENT moving below it while
  the logger.error line stays as unchanged context. The resulting position
  was right, the motion was not.
- 8492: '#6995: RapidPro SMS gateway integration (related testing
  improvements)' - #6995 is 'Adds RapidPro as an SMS Gateway', a feature.
  The title gloss was fine; the relationship parenthetical was the
  mischaracterisation, and relationship parentheticals are exactly what
  the cross-reference audit exempts from checking.
- 9022: 'Added/updated ... and a Sentinel integration spec' read as though
  the integration spec were new. diff-tree at 2e1a05ff17 shows only
  tests/e2e/default/reports/sms-messages.wdio-spec.js added; the
  integration spec was modified (+160/-90), as were the two unit specs.
  Now says which single file was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(#136): correct the revert rationale recorded in 620ea52 (messaging)

620ea52 justified keeping five hand-authored drafts out of this PR partly
on "seven fabricated metric names in 7105". That claim is wrong and an
independent audit caught it.

All seven exist at 7105's anchor 67626fff, as nested keys of the
monitoring response rather than as dotted tokens:

  git -C $CORE show 67626fff:api/src/services/monitoring.js \
    | grep -nE "backlog:|total:|seven_days:|last_hundred:"
  # :293  backlog: sentinelBacklog
  # :325  total: jsonV1.messaging.outgoing.state   <- the claimed rename
  # :326  seven_days: weeklyOutgoingMessageStatus
  # :327  last_hundred: lastHundredCounts

and that PR is what adds failed/delivered to the v1 state counters
(MESSAGE_QUEUE_STATUS_KEYS gains them; the parent had only due/scheduled/
muted). A dotted path like monitoring.messaging.outgoing.seven_days
describes the JSON shape and cannot grep as one token - the same
extraction artifact this branch correctly dismissed four times elsewhere
(sms.clear_failing_schedules, smsparser.parse, nepal-doit-sms,
getOutgoingMessages). I booked it as evidence instead.

So the "19 ungrounded claims" figure was inflated by artifacts of that
class. The decision to defer the five still holds, on evidence that does
survive checking:

- 6995 names RAPIDPRO_URL, RAPIDPRO_TOKEN and handleCallback; all three
  are zero-hit at e9e305d2, where credentials actually come from
  secureSettings.getCredentials('rapidpro:outgoing').
- 3406 contradicts itself: Code Patterns recommends compound view keys
  while also recommending string keys, and the PR emits only msg.uuid and
  task.state - no compound key at all.
- 4374 names three files as touched that its backport commit does not
  touch; 4039 names shared-libs/messaging, absent at its anchor.

Those are real defects in drafts that have never been anchored, and they
still deserve their own review rather than a rider on this one. But the
count was overstated, and the record should say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): 9022 Problem overstated how narrow the old gate was (messaging)

Problem said the context was populated 'only when a shortcode id was
present on the report's fields', which the draft's own Root Cause refutes:
the gate was 'doc.patient_id || doc.fields?.patient_id', so a top-level id
worked too. What it never consulted was the hydrated doc.patient, which is
exactly what the fix adds ('|| doc.patient?.patient_id'). Problem now says
that. Found by one coherence pass of three - the sampling caveat in
practice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#136): bump 9022 lastUpdated, which the previous commit missed (messaging)

1d54af9 rewrote 9022's Problem section and left the stamp at 2026-07-30,
so the branch head failed the stale-timestamp check that 31c8d8c had
documented hours earlier. Second time this cycle after 9407, and for the
same reason both times: the stamp is set from the date of the change being
made, then a later commit to the same file moves 'last edited' past it.
'Touch the file, stamp it today' is the only version of the rule that
survives its own next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Hareet and others added 6 commits August 11, 2026 15:17
…ontacts)

Every item was checked against the PR's own diff and cht-core master before
being changed, not taken on the review's word. All four held up.

**9281 -- the getAll AsyncGenerator was inverted.** The draft said the generator
yields pages and showed a nested loop. It yields individual docs:

  git -C $CORE show refs/verify/pr9281:shared-libs/cht-datasource/src/libs/data-context.ts
  #   getDocumentStream ... : AsyncGenerator<T, void>
  #   for (const doc of docs.data) { yield doc; }
  git -C $CORE show refs/verify/pr9281:.../test/libs/data-context.spec.ts
  #   131: it('yields document one by one'

Rewritten to the flat `for await (const person of Person.v1.getAll(ctx)(q))`
shape. Two facts found while verifying and now recorded: the PR squash-merged
into the `9193-api-endpoints-for-getting-contacts-by-type` feature branch
(`bf8a77da`, not an ancestor of master) and reached master only via #9311
(`34dd0303c`); and the helper was renamed before landing -- at #9311's squash it
is already `getPagedGenerator` in `libs/core.ts`, with `getDocumentStream` absent
and the signature already `AsyncGenerator<Person, null>`. Time-scoped, not
silently corrected to master's shape. The same PR also swapped getPage's numeric
`skip` for a string `cursor` and moved it ahead of `limit`
(`- return fn(personType, limit, skip)` / `+ return fn(personType, cursor, limit)`).

**10043 / 10057 / 9266 / 9281 / 9835 -- data-access, deferred with disclosure.**
The reviewer's own "extend vs use" rule is satisfied: all four anchor PRs touch
`shared-libs/cht-datasource` and nothing else (`git diff-tree --name-only` per
squash). Deferred per the reviewer's own sequencing on #122 -- "one coordinated
schema/taxonomy PR ... Not blocking any single PR" -- and the #123 precedent.
`data-access` is not a valid `domain` today (`agent-memory/schema.json` CHTDomain
enum holds 9 values, none of them it) and PR #152 adds it, open and unmerged, so
re-keying here would race #152 for the same enum value. Each of the five now says
so in its own text rather than leaving the reader to infer it.

**9007 -- Domain Rationale leakage stripped.** "Per the infrastructure pitfall"
is classifier scaffolding; replaced with the substantive reason. While verifying,
the vague `page_size` prose was pinned to the real constant:
`-  private readonly PAGE_SIZE = 50;` / `+  private readonly PAGE_SIZE = 25;`.
Also dropped "verified with a manual quick test" -- PR #9007's body has an
entirely unchecked review checklist and says nothing about manual testing.

**9915 -- the dropped attribution, justified rather than restored.** Round 2
reworded "Reviewer verified the correct workflow (xlsx edit -> xml regeneration)
was followed" to drop "Reviewer". Restoring it would re-assert something the diff
contradicts: of PR #9924's 29 changed `.xml` files only 17 have a same-named
`.xlsx` beside them; the other 12 are the place create/edit forms, expanded from
4 shared `PLACE_TYPE-*.xlsx` templates and edited directly. The section now
states what is checkable from the diff. Counts corrected against the real file
list (50 files, all M -- 29 xml / 21 xlsx; default/app 10->11, covid-19/contact
6->8).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntacts)

Closes the loose end from #123's review, which said `10432` "was relocated to
contacts". It is here -- as `10344-targets-by-contact-id-cht-datasource.md`,
keyed by the issue (#10344) rather than the PR (#10432), which is why looking for
a `10432-*` file finds nothing. Nothing was dropped.

What it needed was scoping, because PR #10432 never merged:

  git -C $CORE merge-base --is-ancestor refs/verify/pr10432 origin/master; echo $?   # 1
  git -C $CORE grep -c byContactUuids origin/master                                  # no output
  git -C $CORE grep -lc byContactUuids refs/verify/pr10432                           # 12 files

The draft asserted all of it as shipped behaviour. It now opens with a banner
saying otherwise and carries `stale: true`.

Two claims in the first draft of that banner were wrong and are fixed here:

- It said no commit in cht-core history references the PR. One does --
  `db9694ef0 feat(#10344): support targets by contact id in cht-datasource
  (#10432)` -- it is simply not reachable from master. Stated that way now.
- It listed `bindGenerator()` among symbols existing "only on that open PR's
  branch". `bindGenerator` is on master in six files, added by epic #10423
  (`622c62542`); #10432 introduces its own independently, the epic not being an
  ancestor of the PR. Master's `target-aggregates.service.ts:35` binds
  `Target.v1.getAll`, not the `TargetInterval.v1.getAll` this draft names. So the
  summary's flat "None of this API exists on master" was also too strong.

Code Patterns and Design Choices credited this proposal with `bindGenerator`;
both now point at #10423. The contact-UUID filtering vocabulary really is
PR-only, and the epic really does lack it (`git grep -c byContactUuids 622c62542`
-> 0), so the rest of the banner stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rounds 3-5 on #123 found more defects outside the reviewer's list than in it, so
all 37 drafts were swept for the same classes. Every finding below was settled
against the anchor PR's own diff or `origin/master`, never reasoned about.
`9264`, `9390` and `10713` came back clean and are untouched.

Worst first.

**9230 -- the whole draft was polarity-inverted.** It said leftover action-bar
logic *prevented* editing a home place and the fix *restored* it. The real bug is
the opposite: the Edit button was wrongly *enabled*.

  git -C $CORE show 9f900220 -- .../contacts-content.component.ts
  #  - canEdit: ... this.userSettings?.facility_id !== this.selectedContact?.doc?._id,
  #  + canEdit: ... !this.userSettings?.facility_id?.includes(...doc?._id),
  # issue #9229: "Old action bar should PREVENT users with multiple facilities
  #               assigned from editing the homeplace"

Once `facility_id` became an array, `['x'] !== 'x'` is always true, so `canEdit`
was always true. Title, summary, Problem, Root Cause, Solution, Design Choices,
Domain Rationale and the `tags` all carried the inversion; all were rewritten
together. Also time-scoped: the action bar and this `canEdit` block were removed
from master by #9361, so `stale: true`.

**8684 -- describes a feature that is not on master at all.** `stale: false` was
the most damaging assertion in the corpus.

  git -C $CORE merge-base --is-ancestor 59a1dbd2 origin/master; echo $?   # 1
  git -C $CORE branch -a --contains 59a1dbd2   # 4.4.1-FR-barcode, 4-4-cares, ...
  for s in search_by_barcode BarcodeDetector can_use_barcode_scanner; do
    git -C $CORE grep -l $s origin/master | wc -l; done                   # 0 0 0

PR #8684 merged into the `4.4.1-FR-barcode` release branch and issue #6669 is
still open. Now `stale: true` with the landing recorded. Two more: it credited
itself with `browser-detector.service.ts` (`M` here; added by #7568 in 2022, this
PR adds one method), and misquoted the telemetry literal --
`barcode_no_detected` where the code says `barcode_not_detected`.

**8984 -- the 50-report cap was described backwards.** The draft said the summary
saw "only the first 50 reports". `search.js` slices the *tail* of the date-sorted
rows, so it keeps the 50 most **recent** and drops the oldest -- which is why
issue #8815 is titled "Only **last** 50 reports for contact are provided" and
reproduces by submitting 50 reports *after* the pregnancy. Corrected in all four
places, plus `search.service.ts` annotated as not modified by this PR and the
separate `DISPLAY_LIMIT = 50` display cap disclosed.

**9601 and 9625 -- prose transcribed from a PR description, not its merged code.**
`9601` named `is_canonical`, a `duplicate_info` section and `context.duplicate_check`;
none exists at the merge commit or on master (the real shapes are an
`[duplicate-contacts]` content-projection slot and a top-level `duplicate_check`).
`9625` claimed freetext search for person and place; the PR adds `getUuidsPage`/
`getUuids` to contact and report only, and creates two controllers rather than
adding endpoints to four. Both were independently flagged by `ground-claims`.

**Attribution corrected on 9295, 9368, 9090, 9177, 10141.** Five drafts credited
themselves with files or symbols another PR introduced -- #9295 called five files
new that #9090 created and are `M` in its own diff; #9368 claimed `/api/v1/person`
when its only added route is `/api/v1/place` and person was already in its parent
tree; #9090 tagged four files it created as #9176's; #9177 put `getByUuid` in
`place.ts` when it lives in `index.ts`; #10141 claimed a public export that
#10157 added. Non-existent namespace members (`Person.v1.getPageByType`,
`Place.v1.getByType`, `Person.V1`) corrected to the real exports, with the
`getDatasource()` facade names distinguished from them.

**Drift disclosed, not silently corrected, on 8684, 9230, 9426, 10777, 10804.**

Each edit was followed by a re-read of the whole draft; that pass caught a
further nine sibling contradictions the individual findings had not named --
`9230`'s Domain Rationale, `8995`'s YAML title, `9295`'s Design Choices and
Testing, `9368`'s Related Issues and Domain Rationale, `9090`'s Related Issues,
`9625`'s summary and Domain Rationale, `10141`'s `concepts` -- which is the
failure mode this exercise is about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…acts)

`8034`, `8074` and `10074` sit in `domains/contacts/issues/` but are already on
`main` and untouched by PR #132 -- `git diff --name-status main...HEAD` covers 34
drafts and none of these three. They are separated here so the promotion diff
stays exactly what it was, and so a reviewer can drop this commit without
disturbing the rest.

They were swept because they are part of the corpus an agent retrieves, and a
defect there is a defect regardless of which PR introduced it.

**10074 -- every path it points at was deleted from master.** Both migration
scripts and their specs went in #10187 (`chore(#9639): remove old migrations
[5.0]`, 2025-08-27), a month after #10085 merged, and the draft's own
`lastUpdated: 2026-03-16` postdates the deletion with no scoping anywhere:

  git -C $CORE ls-tree origin/master api/src/migrations/ | grep -i person   # nothing
  git -C $CORE log --diff-filter=D --format='%h %s' origin/master \
    -- api/src/migrations/extract-person-contacts.js   # 2d44b01e chore(#9639) ... (#10187)

Time-scoped to #10085 rather than rewritten to master's shape; the body still
records what that PR did. Also fixed an inversion: `data-context.js` was said to
"provide" `getLocalDataContext`, which it consumes from `@medic/cht-datasource`
and re-exports the bound result of.

**8034 -- "most permissive setting wins across roles" contradicted its own Design
Choices.** `getDepth()` overwrites `replicatePrimaryContacts` when a role has a
*greater* depth and only ORs it on a tie, so a deeper role with the flag off beats
a shallower role with it on. The PR's own test name says so: "should return most
permissive report depth and replicatePrimaryContacts associated with highest
depth". Also corrected the `do...while` rationale -- the loop exists because a
primary contact is only admissible once its place is in `subjectIds`, not because
of primary-contact chains -- and disclosed that these three services moved under
`api/src/services/replication/` in #10823.

**8074 -- Solution item 5 contradicted item 4.** With parent + type and no
freetext, `generate-search-requests.js` returns a single request with the type
folded into the compound key; there is nothing to intersect. Intersection only
happens when freetext is also present.

**8034 also duplicates a landed data-sync draft, and the gate as documented
cannot see it** -- see the `duplicate-issue` note in the PR reply. Not fixed
here: collapsing two landed drafts across domains is a corpus decision, not a
contacts one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by `verify-drafts --online`, which reached 3 blocking + 8 warnings once it
could run. Round 1's defect was exactly this family -- a PR cited where an issue
belongs -- so these are worth closing rather than waving through.

**Three glosses that described the wrong thing (blocking).** Each was checked
against the real title and body before rewriting:

- `10057` called PR #10056 "companion PR adding the missing test coverage for
  qualifier.ts". It is "feat(#10036): implement `createPerson` for local", whose
  body reads "add support for creating Person Doc in Pouch". Nothing to do with
  qualifier tests.
- `9177` called #8889 "Original proposal for these get-by-uuid endpoints". #8889
  is "Provide API access for online users", still open, and is the broad umbrella
  these endpoints serve rather than a proposal for them specifically.
- `9390`'s gloss for #9311 was substantively CORRECT -- #9311 is the epic squash
  that landed get-persons-by-type and carried #9295's `req.query.personType`, and
  issue #9389's own body links to it saying just that. The checker compares a
  gloss against the PR *title*, so an accurate functional description trips it.
  Rewritten to quote the real subject as well as the relevance, which keeps the
  substance and gives the check something to match. Recording it here because the
  finding was a false positive on the gloss rule, not a defect.

**Seven PRs cited as issues.** `10057#10056`, `9177#9090`, `9177#9176`,
`9390#9311`, `9835#10083/#10081/#10043` now read "PR #N". Two of them were also
the useless gloss "Related code change"; both now say what the PR contributed.
`9601`'s weak gloss for #6363 now quotes the issue's real title.

**Nine stale timestamps.** The eight drafts edited in this batch move to
2026-08-11. `10713` is NOT bumped to today -- it is clean and I never edited it;
its `lastUpdated` was simply never bumped by the commit that last changed it, so
it moves to that commit's date (`f20b746`, 2026-07-16) rather than claiming an
edit that did not happen.

`validate-schema` 91/0 after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tacts)

Two leftovers from the previous commit, both created by that commit.

**`9177`'s replacement gloss for PR #9176 was itself flagged.** I had rewritten
the useless "Related code change" to "created `src/place.ts` with the
`Place`/`PlaceWithLineage` interfaces this PR adds operations to". That is true --
`git show 282faee^:.../src/place.ts` holds the two interfaces and no `export
const` -- but it describes a side effect of #9176 rather than its subject, which
is "add api support for getting a person with lineage by uuid". Same shape as the
`9390` false positive I wrote up last commit: an accurate functional gloss that
shares no vocabulary with the title. Now quotes the real subject and keeps the
relevance, which is what a reader needs anyway.

**`10713`'s timestamp cannot be set backwards.** Last commit moved its
`lastUpdated` to 2026-07-16 -- the date its content actually last changed --
rather than claiming an edit that never happened. But `stale-timestamp` compares
against git's last-edit date, and writing the field IS an edit, so the warning
came straight back pointing at 2026-08-11. Reverting would not help either: the
revert is also an edit. Set to 2026-08-11, which is now simply true of the file.
Its prose is untouched; only the stale metadata moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hareet and others added 11 commits August 11, 2026 17:54
`ground-claims` pass 1 over the committed corpus reported 2 ungrounded and
`check-coherence` pass 1 reported 5 contradictions. Adjudicated one at a time;
three were real, two were tooling defects (fixed on memory/draft-verification,
not papered over here), and one was a genuine ambiguity worth removing anyway.

**Real: `10897` contradicted itself three ways, all created by the earlier fix
to its Problem section.** Correcting Problem to match issue #10878's actual
report -- manual, unhurried navigation during a slow index -- left three
statements asserting the opposite:

- the `title` still said "when rapidly switching pages"; it now says "when
  navigating away ... before it finishes loading", which is what the report
  describes;
- Related Issues asserted "Switching pages too quickly" in the draft's own
  voice. That IS #10878's real title (confirmed against the API), so it is now
  quoted as the issue's title with the report's own contents beside it;
- Design Choices claimed no restructuring of the subscription lifecycle while
  Solution said the callbacks were extracted into two new methods. Both are
  true of different things -- the callbacks moved, the subscribe/teardown did
  not -- and the sentence now says so.

**Real: `8034`'s "any matching role" was ambiguous** between "any role at all"
and "any role tied at the highest depth". Only the second is true. Reworded to
say so outright, which also removes the clash the checker flagged against Code
Patterns.

**Real: `9394` quoted an ungreppable call chain.** The prose wrote
`targetAggregateService.getCurrentTargetDoc()`; the source splits the receiver
and the method across two lines, so no literal search can ever find it. Now
names the method and its service separately -- `getCurrentTargetDoc()` on
`TargetAggregatesService` -- which is both greppable and easier to read.

**Tooling, not content: `9281`.** `getPagedGenerator` came back ungrounded
because the probe checked the anchor, where it genuinely does not exist; the
sentence is about master, where it is at `libs/core.ts:227`. The forward-scope
rescue keys on "on master" appearing in the quote, and the quote is line-bounded
-- the marker had wrapped onto the previous line. Rewrapped so each
master-scoped sentence carries its own marker. The claim never changed.

**Tooling, not content: `10804`.** The checker filed a pair and cleared it in
the same breath -- "a minor framing difference rather than a factual conflict".
Nothing edited here; the withdrawal screen was widened instead.

`validate-schema` 91/0; `verify-drafts` offline 0 blocking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… mine

Three `ground-claims` and three `check-coherence` passes over the frozen corpus,
all 37 drafts, no truncation. Not clean: 3-4 ungrounded and 5-7 contradictions
per pass, and the pass-to-pass variation is the argument for the count -- the
`8984` naming clash appeared in two of three, `9090`'s in one of three.

Recurring in all three coherence passes, and all three are defects the earlier
"fix" commits introduced:

**`9281` (3/3).** My Solution addition -- "replaced getPage's numeric `skip`
with a string `cursor`" -- contradicted the summary and Problem, which both
still said the pre-PR API was cursor-paginated with callers managing cursors.
The Solution is the correct side:

  git -C $CORE show bf8a77da -- .../src/person.ts | grep -E '^[-+].*(skip|cursor)'
  #   -  const assertSkip = (skip: unknown) => {     +  const assertCursor = ...
  #   -    return fn(personType, limit, skip);       +    return fn(personType, cursor, limit);

Before this PR there was no cursor to manage. Summary and Problem now say `skip`.

**`9915` (3/3).** Code Patterns still carried the bald rule "Always edit `.xlsx`
source files first, then regenerate XML via `cht-conf`" while Solution and
Design Choices -- which I had rewritten -- say 12 of the 29 XMLs were edited
directly. Both now describe the two paths.

**`10057` (2/3).** Last commit corrected the Related Issues gloss for PR #10056
to "implements `createPerson` for the local data context" and left Testing still
calling it the PR that supplied qualifier.ts coverage. Testing now agrees.

Also real, from the same rounds:

- **`8984`** named the display cap `DISPLAY_LIMIT` in Solution and
  `DOCS_DISPLAY_LIMIT` in Testing. Master has `DISPLAY_LIMIT`
  (`contacts-content.component.ts:86`); `ground-claims` flagged the other as a
  fabricated symbol in the same round.
- **`9203`** Related Issues said "None directly referenced" while Problem cites
  #9128 as the change that introduced the array shape. Now listed -- and as
  `PR #9128`, because labelling it plainly `#9128` promptly earned a
  `related-ref-is-pr` warning. Fourth time a fix here has generated the next
  finding.
- **`9394`** named `CHTDatasourceAPI` three times. No such symbol exists at the
  anchor or on master; the class is `CHTDatasourceService`. Reworded to describe
  the surface it builds for config scripts.
- **`9295`** Testing said "Extensive tests added" and named two spec files. Every
  test file in that PR is `M`, none `A` -- the added-vs-modified class, caught by
  the deterministic status inference on a draft it had already passed once.
- **`8995`** wrote `_ids`; the code is `_map(reports, '_id')`.
- **`9601`** put `mat-expansion-panel` in the component's `.ts`; it is in the
  `.html` (4 hits).
- **`9090`** wrote `Qualifier.byUuid` as living in `qualifier.ts`. `byUuid` is
  there; the qualified form only appears at call sites. Reworded so the file
  claim is about the symbol it actually contains.

`validate-schema` 91/0. `verify-drafts --online` 0 blocking, 0 unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three ground and three coherence passes on `73502eb`, all 37 drafts. Findings
dropped from 3-4 ungrounded / 5-7 contradictions to **1 / 2**, and all three
distinct findings are closed here.

**`9090` — `Qualifier` is not in `qualifier.ts` (ungrounded in 3 of 3).** Last
round's reword still paired the bare token with that file. The file genuinely
never contains it:

  git -C $CORE grep -cFw Qualifier 59b42e2fa -- .../src/qualifier.ts   # 0
  git -C $CORE grep -c   Qualifier 59b42e2fa -- .../src/qualifier.ts   # 5
        # the 5 are UuidQualifier / isUuidQualifier — substrings, not the name
  git -C $CORE grep -n 'as Qualifier' 59b42e2fa -- .../src/index.ts
        # index.ts:39: export * as Qualifier from './qualifier';

The namespace is created at the re-export, not in the module. Now says so, which
puts `byUuid` in `qualifier.ts` and `Qualifier` in `index.ts` — where each
actually is.

**`9203` — the #9128 gloss I added last commit was backwards (2 of 2).** I wrote
that PR #9128 created "the older-database shape this fix tolerates". It created
the *new* array shape; the legacy `string` is what pre-#9128 databases still
hold, which is the whole point of the fix:

  git -C $CORE log -1 --format='%h %s' c7fbcb1b8
  #   feat(#9116): update user place field in admin to allow setting multiple places (#9128)

Corrected. That is the fifth time in this batch a fix has produced the next
finding, and the third where the checker caught me rather than the pipeline.

**The data-access banners contradicted the Domain Rationale (1 pass each on
`10043` and `9281`).** The banner said the anchor PR extends the library "rather
than a contacts feature" while Domain Rationale argues contacts is the most
specific fit. Both are true of different taxonomies, and neither said so. All
five banners now state it explicitly: primary would be `data-access` with
`contacts` secondary under the proposed scheme, and `contacts` remains the
closest of the nine that exist today. No claim changed; the frame was missing.

`validate-schema` 91/0. `verify-drafts --online` 0 blocking, 0 unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cts)

Round 4: ground came back **0 ungrounded in three consecutive passes**;
coherence was 0 / 1 / 0, and the one finding is real.

The summary said the library exposes "person.getByUuid through both imperative
and declarative APIs". `getByUuid` is only the imperative facade; the declarative
export is `get`:

  git -C $CORE show 59b42e2fa:.../src/person.ts | grep -n 'export const'
  #   32:  export const get = (context: DataContext) => {
  git -C $CORE grep -n getByUuid 59b42e2fa -- shared-libs/cht-datasource/src
  #   index.ts:59:  getByUuid: (uuid) => ctx.bind(Person.v1.get)(Qualifier.byUuid(uuid)),

The Solution and Code Patterns already said this; the summary was the stale side.
It now names both spellings so neither section can be read as the other's
contradiction.

Appearing in one pass of three is what the pass count is for: the same draft's
`Qualifier` claim showed in three of three last round, this one in one of three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 6. Ground stayed at 0 ungrounded across three passes; coherence found two
more, one in two passes of three and one in one of three.

**`9177` (2/3).** The summary said "only get-person existed from #9065" while
Problem said "only get-person/get-person-with-lineage existed". Problem is
right — both were on the tree this PR built from:

  git -C $CORE show 282faee19^:shared-libs/cht-datasource/src/person.ts | grep -n 'export const'
  #   53:  export const get = getPerson(...)
  #   61:  export const getWithLineage = getPerson(...)

**`9835` (1/3).** Root Cause called PR #10083 "the prior attempt" that "had
duplicated validation logic", while Solution credits the same PR with
introducing `input.ts` and parameter-validators for *centralized* validation.
Solution is right; #10083 added the file:

  git -C $CORE diff-tree --no-commit-id --name-status -r f382785be | grep input.ts
  #   A	shared-libs/cht-datasource/src/input.ts

#10083 is the initial implementation this draft describes, not something it
superseded. Root Cause now describes the pre-datasource state — validation
repeated at each call site — without attributing it to the PR that fixed it.

Both are the same shape: a section that enumerates a prior state with "only",
and gets the enumeration wrong. Neither was reachable from the anchor diff alone;
both needed two sections read against each other.

`validate-schema` 91/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 7. Ground 0 / 0 / **2** — the streak broke on the third pass after nine
consecutive clean ones, which is the whole case for not stopping at one.

**`9177`, and this one is mine.** Last round I rewrote the summary to name the
prior state as "get-person and get-person-with-lineage". Those are prose, not
identifiers:

  for s in get-person get-person-with-lineage; do
    git -C $CORE grep -lFw "$s" origin/master | wc -l; done      # 0, 0
  git -C $CORE grep -n 'export const get\b' origin/master -- .../src/person.ts
  #   61:  export const get = ...        69:  export const getWithLineage = ...

Both sections now name `Person.v1.get` and `Person.v1.getWithLineage`, which are
real, greppable, and more use to a reader than a hyphenated paraphrase. Fixing
the count last round introduced a fabricated-symbol pair this round.

**`8074` (coherence, 1/3).** Problem said the widget "only supported searching
contacts by document type, not by parent"; Root Cause said filters were built
"from only contact types and freetext". Root Cause is right — contact freetext
search predates this PR:

  git -C $CORE show 454788537^:shared-libs/search/src/generate-search-requests.js | grep -n freetext
  #   202: const freetextRequests = freetextRequest(filters, 'medic-client/contacts_by_freetext');

That is the third draft in two rounds whose defect is an "only ..." enumeration
of a prior state that omits something — `9177`, `9835`, now `8074`. Worth naming
as a class: the sentence is written to motivate the change, so whatever the
change did not touch tends to get dropped from the list.

`validate-schema` 91/0; `verify-drafts --online` 0 blocking, 0 unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…contacts)

Round 8: ground **0 / 0 / 0**; coherence 1 / 0 / 0, and the one finding is again
a sentence I wrote.

My round-2 rewrite of the summary said the `analytics.getTargetDocs()` entry is
"built by `CHTDatasourceService`". The Solution I wrote in the same commit says
that service only declares an empty-array stub and `contact-summary.service.ts`
supplies the working function. The Solution is right:

  git -C $CORE show bbe5dedd5 -- webapp/src/ts/services/cht-datasource.service.ts
  #   +        analytics: {
  #   +          getTargetDocs: () => ([]),
  git -C $CORE show bbe5dedd5 -- webapp/src/ts/services/contact-summary.service.ts
  #   +    chtScriptApi.v1.analytics.getTargetDocs = () => targetDocs;

The summary now names both halves — declared as a stub by one service,
overwritten by the other before the generator runs — so neither section reads as
the other's contradiction. Correcting a draft's mechanism in one section and
leaving the summary asserting the tidier version of it is the single most
frequent way I have broken these drafts.

`validate-schema` 91/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as one wrong one

Round 9: ground 0 / 1 / 0, coherence 0 / 0 / 1.

**`8995` — `contactViewModelGenerator` is not a thing.** The prose named a
receiver that exists nowhere; `addHeading` is a private method invoked as
`this.addHeading(...)`:

  git -C $CORE grep -nFw contactViewModelGenerator 0ba3adb75          # 0 hits
  git -C $CORE grep -n 'class ContactViewModelGeneratorService\|addHeading(' 0ba3adb75 \
    -- webapp/src/ts/services/contact-view-model-generator.service.ts
  #   37:  export class ContactViewModelGeneratorService {
  #   289:   private async addHeading(reports, forms) {
  #   329:     .then(reports => this.addHeading(reports, forms))

Same shape as `9394`'s `targetAggregateService.getCurrentTargetDoc` and `9177`'s
`get-person`: prose invents a qualified name for something real. Now names the
method and the class it belongs to, both greppable.

**`9264` — nothing was wrong, and it still needed the edit.** Problem cites the
reported symptom (`contact_detail:clinic:load`) and Solution illustrates the
new-style branch with `"hospital"`. Both are accurate — the karma spec really
does exercise `hospital` — but a reader meeting two different types for one fix
has to work out that they are the same code path. Stated outright instead. This
is the "correct-but-unreadable" case: the checker misread it, so a person would.

`validate-schema` 91/0; `verify-drafts --online` 0 blocking, 0 unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(contacts)

Round 10: **coherence 0 / 0 / 0** — the first three-pass clean sweep of that
tier. Ground 1 / 0 / 0.

`10057` said the create path validates the parent "against the allowed `parents`
configured for the new contact's contact_type ... centralized in `src/input.ts`",
which pairs the token with a file that never contains it — at any anchor:

  git -C $CORE ls-tree e0ecefed49 -- shared-libs/cht-datasource/src/input.ts
  #   (empty — the file does not exist at this draft's own anchor)
  git -C $CORE grep -cFw parents 95153376d -- shared-libs/cht-datasource/src/input.ts
  #   0 — nor at #10124's, which is the PR that adds the file
  git -C $CORE grep -n parents e0ecefed49 -- shared-libs/contact-types-utils/src/index.js
  #   38,46,54 — type.parents, where the allow-list actually lives

The behaviour described is real and does live in `input.ts` at #10124; only the
`parents` allow-list is elsewhere — it is a contact-type config array in app
settings, reached through `contact-types-utils`. The sentence now separates the
two, and states outright that `input.ts` postdates this draft's anchor.

Worth noting for the write-up: this is a claim the sibling-anchor fix could not
rescue, because the token is absent from the named file at every anchor in the
cluster. The fix addresses "wrong commit"; this was "wrong file".

`validate-schema` 91/0; `verify-drafts --online` 0 blocking, 0 unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 11: ground **0 / 0 / 0**; coherence 1 / 0 / 1, both on `9264`, both caused
by the edit I made to it last round.

`9264` was flagged in round 9 as correct-but-unreadable: Problem cited the
reported `clinic` symptom, Solution illustrated the new-style branch with
`hospital`, and the checker read the two types as disagreeing. I rewrote the
prose rather than leave a sentence a checker misreads — and attached the `clinic`
case to the **new-style** branch. It belongs to the legacy one:

  git -C $CORE grep -n -A4 'getTypeId = ' origin/master -- shared-libs/contact-types-utils/src/index.js
  #   24:  return doc.type === 'contact' ? doc.contact_type : doc.type;

A legacy `clinic` doc has `type: 'clinic'`, so `type !== 'contact'` and the
function returns `doc.type`. That is also exactly what the draft's own Root Cause
says — `clinic` is one of the legacy hardcoded types with `contact_type`
undefined, which is *why* the old code fell back to the literal `"contact"`.
Attaching it to the new-style branch contradicted the mechanism the draft
correctly explains two paragraphs earlier.

The `clinic` example now sits on the legacy branch and `hospital` on the
new-style one, each labelled with what it is.

This is the sharpest lesson of the run and belongs in the write-up: the draft was
**not wrong** when I touched it, only hard to read. Rewriting unreadable-but-true
prose is not a free action — I made it false, and it took two more passes to find
out. "If a checker misreads it, a person will too" is good advice; it does not
license editing without re-deriving the mechanism first.

`validate-schema` 91/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tacts)

Round 12: ground **0 / 0 / 0**; coherence 1 / 0 / 0.

Solution said the PR adds "a new `contacts_by_primary_contact` view"; Design
Choices said the value-shape change was taken "rather than adding a separate
view, to avoid maintaining two views for the same data". Both are true, of
different views, which is exactly why they read as a contradiction:

  git -C $CORE diff-tree --no-commit-id --name-status -r 80760a6d2 | grep views
  #   M  ddocs/medic-db/medic/views/contacts_by_depth/map.js
  #   A  ddocs/medic-db/medic/views/contacts_by_primary_contact/map.js
  git -C $CORE show 80760a6d2 -- .../contacts_by_depth/map.js
  #   -    var value = doc.patient_id || doc.place_id;
  #   +    var value = { shortcode: ..., primary_contact: ... }

The rejected alternative was a *second depth-keyed view* carrying the same rows;
`contacts_by_primary_contact` answers the reverse question and had nothing to
fold into. Design Choices now says which alternative was rejected and why the
new view is not an instance of it.

Unlike `9264` last round, I re-derived the mechanism from the diff before
touching the prose. That is the step I skipped there, and skipping it is how a
readability edit turned a true draft false.

`validate-schema` 91/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Hareet

Hareet commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Request for re-review

All four of your items are fixed, each re-derived from the PRs' own diffs before
being touched rather than taken on your word — all four held up. Then the same
sweep #123 got turned up a great deal more outside your list than in it,
including one draft whose entire narrative is backwards and one that documents a
feature which is not on master at all.

Two things to know before the findings. The PR body's "44 drafts" is stale
the directory holds 37, and this PR's own diff is 34 of them plus
agent-memory/schema.json (27 added, 7 modified). 10074, 8034 and 8074 are
already on main and untouched by #132; I swept them anyway and committed the
fixes separately so you can drop that commit without disturbing the promotion.

And the gate as we have been running it cannot see cross-domain duplicates
which is the one class you filed first. Details below; it is the most important
thing in this reply.


The gate has been running a weaker dedup check than the docs claim

verify-drafts computes duplicate-issue "across the WHOLE corpus, including
already-landed drafts". It does — but only within the tree it is pointed at, and
the documented pre-push command points it at one domain:

# what the runbook says, and what we have been running:
npm run verify-drafts -- --dir <worktree>/agent-memory/domains/contacts
#   37 drafts checked, 0 blocking

# the same bytes, scanned corpus-wide:
npm run verify-drafts -- --dir <worktree>/agent-memory
#   [duplicate-issue] contacts/issues/8034-replicate-primary-contacts-max-depth.md
#       medic/cht-core#8034 is also claimed by
#       9593-feat8034-replicate-primary-contacts-at-max-depth.md

const dir = path.resolve(REPO_ROOT, opts.dir ?? 'agent-memory') is the whole
story: a narrowed --dir silently downgrades a corpus check to a
within-directory one and still reports "0 blocking". Round 2's "identity/dedup
clean" was produced by that narrowed scan, so it is not evidence about
cross-seeder duplicates either way.

I re-checked your three by hand and all three are genuinely resolved — each
issue now has exactly one claimant:

for iss in 9835 9065 6543; do git grep -l "issueNumber: $iss$" \
  memory/promote-contacts memory/promote-authentication main -- 'agent-memory/domains/*'; done
#   #9835 -> contacts/9835 only        #9065 -> contacts/9090 only
#   #6543 -> authentication/9126 only

The 8034/9593 collision is not introduced by this PR — both files are
already on main, and 8034 is not in #132's diff. It is a landed-corpus defect
and collapsing two drafts across domains is your call, not a contacts one, so I
have not touched it. But it would have shipped unseen, and it will keep shipping
unseen on every domain until either the runbook points --dir at
agent-memory/ or the script warns when the scan root is narrower than the
corpus. I would take the warning.


Your four items

9281 — the getAll inversion, confirmed and fixed. The generator yields
documents, not pages:

git -C $CORE show refs/verify/pr9281:shared-libs/cht-datasource/src/libs/data-context.ts
#   export const getDocumentStream = async function* <S, T>(...): AsyncGenerator<T, void>
#     for (const doc of docs.data) { yield doc; }
git -C $CORE show refs/verify/pr9281:.../test/libs/data-context.spec.ts | grep -n 'it('
#   131: it('yields document one by one'

Rewritten to the flat for await (const person of Person.v1.getAll(ctx)(q))
shape, with the facade test's own assertion cited — it drains the generator and
deep-equals the result against the flat array of people, not a list of pages.

Two things surfaced while verifying that the draft now records. #9281
squash-merged into the 9193-api-endpoints-for-getting-contacts-by-type feature
branch (bf8a77da, not an ancestor of master) and reached master only via
#9311 (34dd0303c) — which is why its source_sha will not resolve in a plain
clone. And the helper was renamed before landing: at #9311's squash it is
already getPagedGenerator in libs/core.ts, getDocumentStream is gone, and
the signature is already AsyncGenerator<Person, null>. That is time-scoped in
the draft, not silently corrected to master's shape. The same PR also replaced
getPage's numeric skip with a string cursor and moved it ahead of limit
(- return fn(personType, limit, skip)+ return fn(personType, cursor, limit)).

10043/10057/9266/9281 plus 9835data-access, deferred with an
explicit disclosure on each.
Your "extend vs use" rule is satisfied exactly for
the four you named: each of their anchor PRs touches shared-libs/cht-datasource
and nothing else.

for pr in 10043 10057 9266 9281; do
  sha=$(git -C $CORE log --all --fixed-strings --grep="(#$pr)" --format=%H | head -1)
  echo "#$pr:"; git -C $CORE diff-tree --no-commit-id --name-only -r "$sha" \
    | sed 's|/[^/]*$||' | sort -u | sed 's/^/   /'
done
#   every line under shared-libs/cht-datasource — no api/, no webapp/

9835 is the fifth and I am treating it as a candidate rather than a proven
extender: it is hand-authored, spans five source_prs, and two of those
(#10083's api controllers) do reach outside the library. It is where the
10022/10246 report-datasource content from #122 landed, which is your own
reason for wanting it in data-access — but the mechanical rule does not settle
it the way it settles the other four.

Deferred on your own sequencing — "one coordinated schema/taxonomy PR … Not
blocking any single PR"
— and the #123 precedent. Two concrete reasons beyond
precedent: data-access is not a valid domain today (the CHTDomain enum
holds nine values, none of them it) and PR #152 already adds it and is open,
so re-keying here means two open PRs racing to add the same enum value; and
until #135 lands union selection, a re-keyed draft drops out of contacts
retrieval entirely. Each of the five now says that in its own text rather than
leaving a reader to wonder why a pure datasource draft sits in contacts. Say the
word and I will move all five — it is a small change now that the reasons are
written down.

9007 — leakage stripped. "Per the infrastructure pitfall" is gone. While
verifying I also pinned the vague page_size prose to the real constant
(- private readonly PAGE_SIZE = 50; / + ... = 25;) and dropped "verified
with a manual quick test" — PR #9007's body has an entirely unchecked review
checklist and mentions no manual testing.

9915 — the dropped attribution: I justified the removal rather than restore
it,
because restoring it would re-assert something the diff contradicts. The
original read "Reviewer verified the correct workflow (xlsx edit → xml
regeneration) was followed". That workflow was not followed for 12 of the 29
changed XMLs:

git -C $CORE diff-tree --no-commit-id --name-status -r 41362a25 | wc -l     # 50, all M
#   29 .xml / 21 .xlsx; of the 29, 17 have a same-named .xlsx changed beside them
#   the other 12 are the place create/edit forms, expanded from 4 shared
#   PLACE_TYPE-*.xlsx templates and edited directly

Re-adding "the correct workflow was followed" would contradict the draft's own
Solution section, which now describes both paths. The Testing section instead
states what is checkable from the diff. The counts were wrong too and are fixed
(default/app 10→11, covid-19/contact 6→8).


The 10432 loose end from #123 — closed, nothing was dropped

Your #122 comment says 10432 "relocated to contacts". It is here. It is filed
under the issue it resolves, not the PR, which is why grepping for a
10432-* file finds nothing:

ls agent-memory/domains/contacts/issues/ | grep 10344
#   10344-targets-by-contact-id-cht-datasource.md      (source_prs: medic/cht-core#10432)

What it did need is scoping, because PR #10432 never merged and the draft
asserted all of it as shipped behaviour:

git -C $CORE merge-base --is-ancestor refs/verify/pr10432 origin/master; echo $?  # 1
git -C $CORE grep -c byContactUuids origin/master                                 # no output
git -C $CORE grep -lc byContactUuids refs/verify/pr10432                          # 12 files

It now opens with a banner saying so and carries stale: true.

Worth flagging that my first version of that banner was wrong in two ways, and
the corrections are in the same commit: it said no commit in cht-core references
the PR (one does — db9694ef0 — it is simply not reachable from master), and it
listed bindGenerator() among symbols existing "only on that open PR's branch".
bindGenerator is on master in six files, added by epic #10423 (622c62542),
and master's target-aggregates.service.ts:35 binds Target.v1.getAll — so the
summary's flat "None of this API exists on master" was also too strong. Same
failure mode as everything else here, committed by the person fixing it.


What the sweep found on drafts you did not name

9230 is inverted end to end — the worst single draft in the corpus. It said
leftover action-bar logic prevented editing a home place and the fix restored
the ability. The real bug is the exact opposite:

git -C $CORE show 9f900220 -- webapp/src/ts/modules/contacts/contacts-content.component.ts
#  - canEdit: ... this.userSettings?.facility_id !== this.selectedContact?.doc?._id,
#  + canEdit: ... !this.userSettings?.facility_id?.includes(...doc?._id),

#9229: "Old action bar should prevent users with multiple facilities
assigned from editing the homeplace" — "See the edit button is enabled … It
should be disabled."

Once facility_id became an array, ['x'] !== 'x' is always true, so canEdit
was always true and the button was wrongly enabled. Title, summary, Problem,
Root Cause, Solution, Design Choices, Domain Rationale and the tags field all
carried the inversion; all were rewritten together. Also time-scoped — the action
bar and this whole block were removed from master by #9361 — so stale: true.

8684 documents a feature that is not on master at all, and claimed
stale: false.

git -C $CORE merge-base --is-ancestor 59a1dbd2 origin/master; echo $?   # 1
git -C $CORE branch -a --contains 59a1dbd2   # 4.4.1-FR-barcode, 4-4-cares, ...
for s in search_by_barcode BarcodeDetector can_use_barcode_scanner isDesktopUserAgent; do
  git -C $CORE grep -l $s origin/master | wc -l; done                   # 0 0 0 0

PR #8684 merged into the 4.4.1-FR-barcode release branch, never into
master, and issue #6669 is still open. Every path in its Related Files does exist
on master — with none of this content — so an agent grepping them finds nothing
and concludes the draft is fabricated. Now stale: true with the landing
recorded and a scope note above Related Files. Two more on the same draft: it
credited itself with browser-detector.service.ts (M in its own diff; created
by #7568 in 2022, this PR adds one method), and misquoted its own telemetry
literal — barcode_no_detected where the code says barcode_not_detected.

8984 had the 50-report cap backwards. The draft said the summary saw "only
the first 50 reports". search.js slices the tail of the date-sorted rows, so
it keeps the 50 most recent and drops the oldest — which is why issue #8815
is titled "Only last 50 reports for contact are provided" and reproduces by
submitting 50 reports after the pregnancy registration. Corrected in all four
places. Also: search.service.ts was in Related Files but is absent from the
PR's diff, and the same PR adds a separate DISPLAY_LIMIT = 50 render cap that
the draft never mentioned — the opposite direction from its own headline.

9601 and 9625 transcribe a PR description rather than its merged code.
9601 named is_canonical, a duplicate_info section and
context.duplicate_check; none of the three exists at the merge commit or on
master (the real shapes are an [duplicate-contacts] content-projection slot and
a top-level duplicate_check on the form doc). 9625 claimed freetext
search for person and place; the PR adds getUuidsPage/getUuids to contact and
report only, and creates two controllers rather than adding endpoints to four.

Five drafts credited themselves with another PR's work — your #10324
telemetryKey class. 9295 called five files new that #9090 created and that
are M in its own diff; 9368 claimed /api/v1/person when its only added
route is /api/v1/place and person was already in its parent tree; 9090 tagged
four files it created as #9176's; 9177 put getByUuid in place.ts when it
lives in index.ts; 10141 claimed a public export that #10157 added. Several
non-existent namespace members went with them (Person.v1.getPageByType,
Place.v1.getByType, Person.V1) — all are getDatasource() facade names, not
v1 namespace exports, and the drafts now distinguish the two.

10074, 8034, 8074 (the three landed drafts) each had a real defect:
every path 10074 names was deleted from master by #10187 a month after its own
PR merged, with no scoping; 8034's "most permissive wins across roles"
contradicted its own Design Choices and the PR's own test name; 8074's Solution
item 5 contradicted item 4.

Nine further contradictions surfaced only on the re-read after a fix —
9230's Domain Rationale, 8995's YAML title, 9295's Design Choices and
Testing, 9368's Related Issues and Domain Rationale, 9090's Related Issues,
9625's summary and Domain Rationale, 10141's concepts. That remains the
highest-yield class and it is generated by the act of fixing.

9264, 9390 and 10713 came back clean and are untouched apart from 9390's
Related Issues labelling.


What twelve rounds of the gate found after the hand sweep

The section above is what reading the drafts against the diffs produced. The gate
then found two dozen more on bytes I had already called clean, across twelve
rounds. Worth reporting because the distribution is the argument for the pass
count, not the total.

Nine of the nineteen were mine — introduced by the commits that fixed your
items and the swept siblings. Six are one shape: corrected the mechanism in the
detailed section, left the summary asserting the tidier version
. 9281's
summary still called the pre-PR API cursor-paginated after I documented that this
PR replaced skip with cursor. 9915's Code Patterns still said "always
regenerate" after I documented the 12 hand-edited XMLs. 9394's summary credited
CHTDatasourceService with a function the same commit described as an empty stub.

Three more classes worth naming:

  • Prose invents a qualified name for something real. 9394's
    targetAggregateService.getCurrentTargetDoc, 9177's get-person, 8995's
    contactViewModelGenerator — none exists; each names a real thing with an
    invented receiver or a hyphenated paraphrase. All three came from my edits.
  • "Only X existed" enumerations of a prior state, wrong. 9177, 9835 and
    8074 in two consecutive rounds. Structural rather than careless: those
    sentences exist to motivate the change, so whatever the change did not touch
    drops out of the list. 9835's was the worst — it attributed duplicated
    validation to the very PR that centralised it (A input.ts in #10083's diff).
  • Editing correct-but-unreadable prose is not free. 9264 was flagged as a
    readability problem; I rewrote it and attached the reported clinic case to
    the new-style branch. getTypeId is
    doc.type === 'contact' ? doc.contact_type : doc.type, so a legacy clinic
    doc takes the other branch — which the draft's own Root Cause already said.
    I made a true draft false, and it took two further passes to notice. Your
    "if a checker misreads it, a person will too" is right; it does not license
    editing without re-deriving the mechanism first, which is the step I skipped.

Round 7 is the single best argument for not stopping early. ground-claims
had been clean for nine consecutive passes over three commits; pass 3 of round 7
then produced two fabricated symbols on 9177. A three-pass bar taken at round 4
would have shipped them.


Five tooling defects this corpus exposed, all fixed with tests

A cluster draft has more than one anchor, and only the first was used.
source_prs[] lists every PR a collapsed or hand-authored draft covers, but
anchorFor takes the first entry as canonical. file-touched already consulted
the siblings; symbol, symbol-in-file and path-exists did not. 9835 lists
five source_prs, resolved to #10022 (the earliest), and returned 12
ungrounded claims — every one of them false
:

git -C $CORE grep -c 'minifyDoc\|assertSameParentLineage\|getUpdatedContact' \
  origin/master -- shared-libs/cht-datasource/src/local/libs/lineage.ts   # 3
git -C $CORE grep -c assertPermissions origin/master -- api/src/controllers/person.js  # 4

Every symbol is real, on master, in exactly the file the draft names — they
simply arrived in #10081/#10083/#10222/#10246. Tree-scoped claims now retry at
each sibling anchor and say which one settled them. A single-PR draft has no
siblings, so no verdict there can change; that is a test.

Paths in this repo were adjudicated against cht-core. The data-access
banners cite agent-memory/schema.json; cht-core has no agent-memory/ tree, so
a file that plainly exists came back ungrounded on four drafts. Now
unverifiable — not our tree to settle.

A sentence that scopes itself to before the change was judged after it.
FORWARD_SCOPED rescues "on master" claims by re-checking the current tree;
there was no mirror for the past, and the Problem/Root-Cause parent retry only
fires when extraction tags a claim pre-fix, which it often does not. 9394
said "before this PR contacts.effects.ts called getCurrentTargetDoc()":

git -C $CORE grep -c getCurrentTargetDoc bbe5dedd5  -- webapp/src/ts/   # 0
git -C $CORE grep -n getCurrentTargetDoc bbe5dedd5^ -- webapp/src/ts/
#   effects/contacts.effects.ts:182:      .getCurrentTargetDoc(this.selectedContact)

Real at the parent, deleted by the PR — a true sentence reported as fabricated.
An explicit "before this PR" / "used to" / "previously" now re-checks the parent.

check-coherence still counted a pair the model had withdrawn. The existing
screen wants a negation beside the noun ("These do not conflict"). 10804's
rationale read "a minor framing difference rather than a factual conflict"
same withdrawal, different grammar, so it was filed and I was sent to adjudicate
something already cleared. That is the exact defect the screen was added for, in
its other form.

All four on memory/draft-verification with tests; npm test 1214 passing,
tsc --noEmit and eslint . clean. eslint . needed its own commit to be
green at all: outputs/ is gitignored but was not eslint-ignored, so a stray
.ts left there by an earlier investigation fails the gate for everyone.

The enumerate-claims status inference held up on a new corpus. It inferred
10 statuses across contacts, all modified, and all 10 match the PRs' real
--name-status. 0 false positives. The tuning generalised off
tasks-and-targets; I found nothing to tighten.


Gate

Verified bytes: memory/promote-contacts @ 2be6148, clean tree.

tier result passes
validate-schema 91 passed / 0 failed deterministic
verify-drafts (offline) 0 blocking, 1 warning deterministic
verify-drafts --online 0 blocking, 1 warning, 0 unverified, rc=0 deterministic
ground-claims 0 ungrounded, ~717 grounded, 33 unverifiable, 0 anchor-unusable, 0 unmatched-snippet 3 consecutive clean
check-coherence 0 contradictions 3 consecutive clean

Both LLM tiers clean three times each on the same frozen commit, and all six
logs carry zero truncation markers — every pass really did cover all 37 drafts,
which is not something I could say of the first coherence run.

That took 36 ground passes and 39 coherence passes across 12 rounds, because
each round's fixes changed the bytes and restarted the count. The per-round yield:

round ground coherence
1 (pre-fix) 38 ungrounded
2 3, 4, 4 6, 5, 7
3 1, 1, 1 2, 2
4 0, 0, 0 0, 1, 0
5 0, 0, 0 0, 0, 1 → tooling
7 0, 0, 2 0, 1, 0
8–12 mostly 0 one single-pass hit per round
13 0, 0, 0 0, 0, 0

Round 7 is the one to look at. Ground had been clean for nine consecutive
passes across three commits, then pass 3 turned up two fabricated symbols. A
three-pass bar would have passed that corpus at round 4. Round 12's coherence
finding (8034's two views) appeared in one pass of three.

The 33 unverifiable are not a pass, and they are concentrated. Twenty-nine
of them are on the three drafts with no anchor metadata at all — 10074 (13),
8034 (9), 8074 (7) — which are also the three already on main and outside
this PR's diff. The remaining four are the agent-memory/schema.json references
in the data-access banners, which the probe now declines to judge because
cht-core has no such tree. Nothing in the 34 drafts this PR actually changes is
sitting on an unresolved anchor.

The single remaining warning is uniform-domain-fit, which is structural — see
the disclosure list.

Worth putting in the runbook: authenticate gh before running --online.
Anonymous, the online tier gets GitHub's 60 requests/hour and a 37-draft domain
exhausts it, so runs come back with unverified counts that look like content
problems and rc=3. With a token it is 5,000/hour — the same scan returns
0 unverified, rc=0 in one go. No code change is needed: gh-classify already
tries gh api first and only falls back to anonymous curl, so exporting
GH_TOKEN is enough. My first three online runs were all degraded by this and I
initially mis-read it as the tier being unavailable on a host without gh.

One caveat about reading these logs. An early coherence run reported
"37 drafts" while 17 of them carried coherence check failed: You've hit your weekly limit — never actually checked. The summary line looks identical to a
clean sweep. Every pass counted above was checked for that marker and none has
one, but the summary line alone will not tell you.

The sequence below is the point, not the final number:

  • verify-drafts --online went 3 blocking + 18 warnings → 1 blocking + 2
    warnings → 0 blocking
    , and the middle step is the interesting one: the
    remaining blocker was one I had just written.
    Replacing 9177's useless "Related code
    change" with "created src/place.ts with the Place/PlaceWithLineage
    interfaces" is true — but it describes a side effect of #9176 rather than its
    subject, and tripped the same title-matching rule that had produced the 9390
    false positive one commit earlier. I had written up that blind spot and then
    walked into it. Fixing a draft is how you generate the next finding, on the
    tooling as much as the prose.

  • The very first ground-claims pass reported 38 ungrounded across 13
    drafts. Adjudicating them by hand turned 16 into tooling defects (12 on 9835,
    4 agent-memory/ paths) and confirmed the rest.

  • Pass 1 on the already hand-cleaned, committed corpus still found 2
    ungrounded and 5 contradictions
    . Five of those seven were content fixes a
    human pass had missed — including 10897 contradicting itself three ways, all
    three created by my own earlier fix to its Problem section. The other two were
    fresh tooling defects (below). That is precisely the argument for a pass count
    above one, and it is the argument I cannot finish making here.

  • Four of the readers' independent findings were corroborated by the gate on
    the same bytes
    9601's duplicate_info/is_canonical, 9625's
    person/place search, 10777's role="button", and 9394's targetDocs. Two
    methods agreeing is the strongest signal available here.

  • verify-drafts --online could not run at all until I checked why: it shells
    out through gh-classify, which tries curl first and gh second. There is
    no gh on this machine, and the first thing I assumed was that the online tier
    was simply unavailable. It is not — it works on curl alone.

A blind spot you should know about, because it cost a real edit. The
related-ref-gloss-mismatch rule compares a gloss against the referenced item's
title. 9390 glossed #9311 as "the original PR that added get-persons-by-type
support and introduced the inconsistent personType parameter". That is
correct — it is what issue #9389's own body says — but it shares almost no
vocabulary with #9311's title, so the rule fired blocking. I rewrote the gloss to
quote the real subject and keep the accurate substance, which is the right
outcome for a reader, but the finding itself was a false positive. Worth knowing
before someone deletes a true gloss to make a gate go green.


What I disclosed rather than fixed

  • Convergence itself. One ground-claims pass and one partial
    check-coherence pass, not three of each, blocked on the weekly subscription
    limit. Everything below this line is a judgement made against a corpus that has
    had one machine pass, and pass 1 on frozen bytes found five real content
    defects — so I would expect a further pass to find something, not nothing.

  • The data-access cluster — deferred, per your own sequencing, now with the
    reasons written into all five drafts. Reversible in one commit.

  • 8034/9593 cross-domain duplicate — a landed-corpus decision, and the
    two drafts sit in different domains. Not a contacts fix.

  • stale-as-written drift on 8684, 9230, 9426, 10777, 10804,
    10074, 8034
    — time-scoped and disclosed, never rewritten to master's
    shape.

  • 10344 kept rather than deleted: it records the design reasoning behind an
    open proposal, and now says so in its first paragraph.

  • The two coexisting draft schemas — untouched, as you asked. One consequence
    worth naming: uniform-domain-fit reports "all 27 drafts self-report
    domainFit: strong", not 37, because the 10 hand-authored drafts have no
    domainFit at all. The warning is real but it is silently sampling 73% of the
    corpus, and the same holes mean roughly a third of these drafts anchor on a
    bare source_prs list — weaker evidence than the 25 fully-anchored drafts on
    chore(memory): promote strong-fit tasks-and-targets drafts from memory-pipeline for review #123, and the direct cause of the 9835 false-positive cluster above.

  • Filenames still encode the PR number. Not blocking, per your note — but
    9230's filename now reads
    9230-fix9229-old-action-bar-prevents-editing-home-places.md against a draft
    that correctly says the opposite. The slug comes from the PR title, which was
    itself ambiguous. Flagging it because a filename is what an agent greps first.

One correction to the brief I was working from: the hand-authored set is 10
drafts, not 11
10897 carries source_pr, source_sha, domainFit and
confidence and is machine-distilled. 37 − 27 = 10 falls straight out of the
uniform-domain-fit count.


Commits

memory/promote-contacts (2be6148), 18 commits grouped by cause so any
group can be dropped on its own:

commits what
9c53874 your four review items, each re-derived first
dc0aef7 10344 scoped to the open proposal — the 10432 loose end
ca418f6 the 23 swept drafts you did not name
55dbd0a 10074/8034/8074 — landed, outside this PR's diff
c09d099, 8991edb Related Issues refs, glosses, timestamps
25683132be6148 (12) one per gate round, each naming what it found and which side the diff supported

memory/draft-verification (8df2b78):

commit what
e7d40b7 a cluster draft has more than one anchor (+ agent-memory/ paths)
4e8c0c9 keep gitignored run artifacts out of eslint .
101613b judge a "before this PR" sentence at the parent (+ downgrade withdrawals)
6fa6949 docs: authenticate gh before running the online tier
8df2b78 a pair withdrawn as "not exclusive" is still withdrawn

npm test 1217 passing, tsc --noEmit and eslint . clean.
agent-memory/_skipped.ndjson reverted, not committed.

@Hareet
Hareet requested a review from sugat009 August 12, 2026 03:59
@Hareet Hareet self-assigned this Aug 12, 2026
@Hareet Hareet moved this from Todo to In Review in CHT Multi-Agent System (cht-agent) Aug 12, 2026

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3. All five of my round-2 items are resolved, several beyond what I asked: 9281 is now precise and master-scoped, 9915 was re-derived instead of patched and its new numbers verify exactly (PR 9924 is 50 files, 29 xml + 21 xlsx, zero adds or deletes), and the out-of-diff sweep in 55dbd0a is legitimate corpus repair whose 10074 deleted-migrations claim checks out (#10187, zero person migrations on master).

Requesting changes on five items, each verified with the full protocol (head quote, anchor, master, and the commit history between them, so drift and error are distinguished) and each carrying a suggested rewording. Three are blocking: the 10057 input.ts centralization claim is false at every point in history (the file does not exist at this draft's anchor, the helpers were per-module at #10124, and master's input.ts is types-only), the 9835 permission enumeration is wrong for five of six write endpoints, and 8684's "all listed files still exist on master" fails for config/standard/app_settings.json.

Two notes, neither blocking: the 55dbd0a "drop this commit" separability claim is stale now that later commits re-touched 8034/8074, worth a one-line follow-up in the PR description; and the same epic-branch template suggestion as on #122 applies to 10344's banner.

- Implemented `createPlace` in the local place module (`v1.createPlace` curried over the LocalDataContext, writing through the medic DB service) (PR #10065).
- Added `createPlace` to the public place module (`src/place.ts`) with matching local and remote adapter implementations, exported via `src/index.ts`, input validation in `src/libs/parameter-validators.ts`, and a new `api/src/controllers/place.js` controller registered in `api/src/routing.js`, built on prerequisite PR #10065 (PR #10089).
- Extended the local place input qualifier to validate the `parent` field so places cannot be qualified without a valid parent reference (PR #10108).
- On create, fetch the referenced parent contact and check its `contact_type` against the parent types the new contact's own type permits — that allow-list is the `parents` array on the contact-type config in app settings, read through `shared-libs/contact-types-utils`, not a field of the datasource. The `parent`/`contact` field is then stored as a dehydrated/minified nested lineage object (`{_id, parent: {_id, parent: ...}}`) instead of a bare string. The fetch, the check and the shaping are centralized in `src/input.ts` — a file PR #10124 adds, so none of this is present at this draft's own anchor — and reused by local person/place/report creation (place/report `contact` storage needs no parent-type validation) (PR #10124).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): This misplaces PR #10124's core logic and miscredits the file. src/input.ts never contained the parent fetch, the contact_type check, or the lineage shaping, and #10124 did not add the file.

What the history shows:

  • input.ts was added by PR #10094 (commit 806456120, 2025-06-27) and at that point held only input-shape validation (validateContactInput, validateReportInput, reported_date format checks).
  • PR #10124's entire diff to input.ts is +6/-1 (git diff 95153376d^1 95153376d -- shared-libs/cht-datasource/src/input.ts): it adds contact: string to ReportInput and a required-contact check.
  • In #10124 the parent fetch, contact_type-vs-allowed-parents check, and dehydrated-lineage construction are module-local helpers duplicated per file: ensureHasValidParentFieldAndReturnParentDoc/validatePersonParent/appendParent in local/person.ts, a separate copy of ensureHasValidParentFieldAndReturnParentDoc plus validateParentPresence/appendParent/appendContact in local/place.ts, and appendContact in local/report.ts. Nothing is centralized or reused across modules at that point.
  • Centralization came later on the feature branch (PR #10308, PR #10522): the logic moved into local/libs/lineage.ts (assertHasValidParentType, minifyLineage) and local/libs/doc.ts (getDocsByIds), and input.ts was reduced to types only before the branch squash-merged to master as PR #10083.

Suggested rewrite for lines 75 and 83: attribute the fetch/check/shaping to per-module helpers in local/person.ts, local/place.ts, and local/report.ts (duplicated in #10124, centralized into local/libs/lineage.ts by later feature-branch refactors). For line 110, change the attribution to shared-libs/cht-datasource/src/input.ts (added in PR #10094, touched by PR #10124). The existing caveat that none of this exists at the draft's own anchor (e0ecefed4) is correct and can stay.

lastUpdated: 2026-03-16
summary: Added cht-datasource APIs to query target interval documents filtered by contact UUIDs, enabling the target aggregates service to fetch only supervised contacts' targets instead of all targets for a reporting period.
lastUpdated: 2026-08-11
summary: 'PROPOSED (open PR #10432, not merged): cht-datasource APIs to query target interval documents filtered by contact UUIDs, so the target aggregates service could fetch only supervised contacts'' targets instead of all targets for a reporting period. The contact-UUID filtering described here is on no branch but the PR''s own; the one piece that did land — binding TargetAggregatesService to cht-datasource via bindGenerator — arrived separately under epic #10423, against Target.v1.getAll rather than TargetInterval.v1.getAll.'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (non-blocking): the state description is inverted, though the guidance is right and should stay. PR #10432 is not an "open PR ... not merged": GitHub has it merged=true on 2025-12-19 into the epic branch 10140_previous-month-targets. What actually happened is sharper: it merged into the epic, then its content was dropped before the squash. #10423's file list carries zero of its files, the contact-UUID vocabulary is zero-hit on master, and the epic branch has since been deleted, so the PR's own head is indeed the only place the code exists. "Do not treat as available API" stays correct. Suggested banner: "Merged into the 10140_previous-month-targets epic branch on 2025-12-19 (PR #10432), but dropped before the epic's squash: #10423 carries none of its files and the vocabulary is zero-hit on master. Do not treat as available API." For the record, the banner's other claim verified cleanly: master's target-aggregates.service.ts:35 is bindGenerator(Target.v1.getAll), introduced by the #10423 squash, and TargetInterval.v1.getAll exists nowhere on master.


## Related Files

Paths as touched on the 4.4.1-FR-barcode branch at the #8684 anchor (2023-11-16). All of them still exist on master, but none carries any barcode code there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): Line 73 says all listed files "still exist on master", but config/standard/app_settings.json does not. The entire standard config was removed from cht-core by medic/cht-core#8762 (issue #8757, commit 3f7f6d6e3, merged 2024-01-10); config/standard/ on origin/master now contains only a readme.md pointer. Verified the other 16 listed paths do exist on origin/master (6eb2ac0f1) with no barcode code, and all 17 existed at the draft's anchor 59a1dbd2, so only this sentence needs fixing. Suggested wording: "All of them still exist on master except config/standard/app_settings.json (the standard config was removed by #8762 in January 2024), and none of the survivors carries any barcode code there."

@@ -60,16 +80,22 @@ PR #10522 implemented create/update APIs while deeply refactoring the internal a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking) [agent-memory/domains/contacts/issues/9835-cht-datasource-create-update-contacts.md:76]: The permission enumeration is wrong for five of the six write endpoints. At the #10083 merge commit (f382785be) and unchanged on current master (6eb2ac0f1), the checks are: person create hasAny ['can_create_people', 'can_edit'] (person.js:152), person update hasAny ['can_update_people', 'can_edit'] (person.js:202), place create hasAny ['can_create_places', 'can_edit'] (place.js:151), place update hasAny ['can_update_places', 'can_edit'] (place.js:209), report create hasAny ['can_create_records', 'can_edit'] (report.js:268), report update hasAny ['can_update_reports', 'can_edit'] (report.js:326), all with isOnline: true. The removed read permission was also can_view_reports for report endpoints, not can_view_contacts. Suggested replacement: "Permission fix (PR #10522): write endpoints no longer require the read permission (can_view_contacts for person/place, can_view_reports for report); each requires an online user with can_edit or the entity-specific write permission (can_create_people/can_update_people, can_create_places/can_update_places, can_create_records/can_update_reports)." Relatedly, the Solution paragraph's claim that PR #10222 made person/place/report honor can_edit is imprecise: #10222's diff adds can_edit for person and place only; report gained can_edit in #10522, which also fixed report update's constant from can_update_records to can_update_reports.

suggestion (non-blocking) [agent-memory/domains/contacts/issues/9835-cht-datasource-create-update-contacts.md:11]: The Solution section leads with PR #10522 (feat(#9835): refactor create/update cht-datasource apis, merged into the feature branch 2026-03-03) and describes its changes in detail, but source_prs omits it. Add "medic/cht-core#10522" to source_prs.

(Nitpick about the Domain note's "its anchor PR" wording: drop. The draft has an identifiable singular anchor PR, #10083, the only source PR merged to master; the claim attached to it is true, and the identical boilerplate appears in sibling multi-PR drafts 10043 and 10057.)

(Anchored here because line 76, where the enumeration lives, is outside this round's diff hunks.)


## Solution

Added Person.v1.getPage(limit, skip) to the cht-datasource public API with limit/skip defaulting to 100/0. Implemented local (PouchDB-backed) and remote (API-backed) variants behind the shared person facade, extended the core libs, doc, lineage, and qualifier helpers to support pagination, and exported the new function from index.ts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (non-blocking): The stated signature Person.v1.getPage(limit, skip) omits the required first argument. At this draft's anchor (b20dc2297, PR #9266's merge commit), the curried function is (personType: ContactTypeQualifier, limit = 100, skip = 0) and assertTypeQualifier(personType) throws when the qualifier is missing, so a two-argument call was never valid, even as of this PR. The Design Choices claim that "callers can omit arguments" (line 77) holds for limit/skip only, never for personType. Suggestion: write the signature as Person.v1.getPage(personType, limit, skip) and state that personType is a mandatory ContactTypeQualifier (see qualifier.ts, already listed in entities). Separately, since the frontmatter says stale: false, consider a one-line drift note: the skip parameter is accurate for this PR, but PR #9281 replaced it with cursor paging on the same feature branch before it reached master (34dd0303c via #9311), and current master is (personType, cursor = null, limit = DEFAULT_DOCS_PAGE_LIMIT).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants