Skip to content

chore(memory): promote strong-fit data-sync drafts for review - #129

Merged
Hareet merged 10 commits into
mainfrom
memory/promote-data-sync
Jul 9, 2026
Merged

Hareet merged 10 commits into
mainfrom
memory/promote-data-sync

Conversation

@Hareet

@Hareet Hareet commented Jun 24, 2026

Copy link
Copy Markdown
Member

Promotes 14 strong-fit data-sync drafts from agent-memory/_pending/ into agent-memory/domains/data-sync/issues/ for squad content review.

Categories: bug (6), improvement (4), feature (4)
Themes: replication keys & primary-contact depth, purging/pre-purge counts, Nouveau/offline view design docs, service-worker UI extensions.

All 14 carry domainFit: strong + a ## Domain Rationale section. 38 weak-fit drafts deferred to Stream C — data-sync was the largest catch-all in this run (73% weak), so this PR is the distilled minority; the over-broad-domain question is for later review.

@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 6 of 14 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:

  • 8773-fix6299-trigger-sync... stores issueNumber: 8773 / issueUrl: .../issues/8773, but 8773 is the merge PR ("fix(#6299): trigger sync if something is left to sync"). The real issue is #6299 (closed): "Sync status sometimes says all reports synced when there are changes yet to sync".
  • 10776-perf10749-move-tasksbycontact... stores issueNumber: 10776 / issueUrl: .../issues/10776, but 10776 is the merge PR ("perf(10749): move tasks_by_contact view to offline-only design document"). The real issue is #10749 (closed): "Move tasks_by_contact to offline clients only".

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:

  • Issue #10792 is promoted 3 times (10793, 10798, 10799), one draft per merge PR for the single fix.

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

Hareet commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Good catch!

I noted that the pipeline is PR-centric but the schema is issue-based. Thanks for the dive and verification

Root cause

The scraper only harvests linked issues from Fixes/Closes/Resolves #N patterns in the PR body. Per its own header docs, it ignores:

  • the fix(#N): / feat(#N): PR title convention, and
  • GitHub's sidebar-linked issues (the GraphQL closingIssuesReferences field).

When that body search returned nothing, the distiller silently aliased the PR number as the issue number:

// src/scripts/distiller.ts:267
const issueNumber = pr.linkedIssues[0]?.number ?? pr.prNumber; // ← the bug

So id, issueNumber, and issueUrl all received the merge-PR number, while source_pr (correctly) retained the PR. The PR-vs-issue distinction was lost in three of the schema's identity fields.

Detection rule: a draft is affected when frontmatter issueNumber equals the number in source_pr (i.e. the fallback fired).


What data we retained

two retained signals are sufficient to re-link deterministically:

Signal Where Reliability
source_pr (PR number) frontmatter of every draft authoritative, already correct
Resolved-issue token filename slug, e.g. 8773-fix6299-… derived from the original PR title, independent of the buggy linkedIssues path

The frontmatter title field is an LLM-paraphrased description and usually does not contain the issue number — the filename is the better retained artifact.


Blast radius (all 261 promoted drafts)

Category Count Recoverable how
Already correct (issueNumber ≠ PR) 125 —
Broken, auto-fixable offline from filename token 130 pure local metadata rewrite
Broken, issueNumber == PR with no clean filename token 6 re-fetch by source_pr, or manual

136 of 261 are affected; 130 (96%) are fixable from retained data with zero network/LLM. This matches the reviewer's observed ratio (~60 of 107).

Per-domain (affected count = token-fixable shown; tokenless 6 are spread across domains, e.g. featna/app-skeleton slugs):

Domain files broken (token-fixable)
messaging 17 12
infrastructure 49 20
forms-and-reports 47 25
tasks-and-targets 35 14
authentication 39 18
contacts 44 31
interoperability 6 1
configuration 10 4
data-sync 14 5

Options

Option 1 — Offline re-link from filename token

Script parses <pr>-<type><issue>- per file and rewrites id/issueNumber/issueUrl to the real issue (keeping source_pr).

  • Pros: zero cost, instant, fully deterministic from retained data, reviewable as a pure metadata diff.
  • Cons: relies on the filename convention; the 6 tokenless cases (featna, bare slug, etc.) must be flagged for manual/lookup handling. Filename token is a secondary source, not GitHub-authoritative.

Option 2 — Deterministic re-fetch (no LLM)

Keyed by source_pr, call gh pr view <pr> --json title,closingIssuesReferences,body to get the authoritative resolved issue (including sidebar links the scraper missed), then apply the same metadata-only rewrite.

  • Pros: authoritative, handles every case incl. sidebar links, deterministic, cheap (~107 gh calls), no re-distillation. Effectively "apply the upstream fix as a post-process over existing drafts."
  • Cons: needs gh auth + network.

Option 3 — Fix distiller/scraper + re-run full pipeline (reviewer's literal ask)

Fix the source, then re-scrape and re-distill the affected PRs.

  • Pros: corrects the source for all future runs; regenerates everything consistently.
  • Cons: most expensive — re-runs LLM distillation, regenerates body content that is already reviewed-as-faithful, risks content drift, and disturbs the already-open PRs. Overkill, since only 3 metadata fields are wrong.

Recommendation — hybrid (no pipeline re-run)

The body content is independent of this bug, so re-running distillation (Option 3) is unnecessary and risky.

  1. Fix the source for future runs: add title-(#N) parsing + GraphQL closingIssuesReferences to the scraper, and stop the distiller silently aliasing PR→issue — set issueNumber from the authoritative link, and when there genuinely is none, leave it null/flagged rather than defaulting to the PR number (keep source_pr separate).
  2. Re-link existing drafts with a one-off deterministic script — Option 2 as the authoritative pass, using the Option 1 filename token as a built-in cross-check so every rewrite is verifiable. Metadata-only diff, no body changes.
  3. Validate + ship: npm run validate-schema → --force-with-lease the affected PR branches → request re-review. This unblocks dedup-by-issue-ID (Context analysis agent doesn't load memory-pipeline drafts (wrong path + schema) #135).

I'm going to run 1 on this domain and verify a few of them. re-running the pipeline isn't bad, but uses my session limit in an hour and ends my weekly limit early

Base automatically changed from seeding-claude-cli-v2 to main June 26, 2026 19:26
@Hareet
Hareet force-pushed the memory/promote-data-sync branch from 2976cbb to 7a9b3b0 Compare June 28, 2026 01:30
Hareet and others added 2 commits June 27, 2026 20:08
…ref filter, Sonar fixes

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

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

Hareet commented Jun 28, 2026 •

Copy link
Copy Markdown
Member Author

This is ready for another look, @sugat009

What changed

Forward fix: the scraper now resolves linked issues from GitHub's closingIssuesReferences (sidebar) → the PR title scope type(#N): → the body Fixes/Closes/Resolves, and the distiller no longer falls back to the PR number — a PR that closes no tracked issue is flagged for human triage instead. The pipeline and the one-off relink share a single issue-linkage module, so they can't drift apart.

Existing drafts: relinked deterministically rather than re-distilled — the body content was already faithful, so the one-off tool rewrites only the three identity fields (id/issueNumber/issueUrl), keyed by source_pr against gh closingIssuesReferences, with the filename token as a cross-check. The diff is metadata-only (verified: exactly 3 lines per file, no body changes).

This PR (data-sync): 5 drafts relinked to their real issues (e.g. 8773 → #6299), 0 flagged. The 10793/10798/10799 drafts are a genuine three-PRs-to-one-issue cluster (all close #10792) and are left intact — source_pr is now the stable provenance key, so these become the worklist for dedup-by-issue-ID (#135) rather than mis-links.

The remaining domains follow in their own PRs using the same tool (after this one is approved); their edge cases (suspect mismatches, PRs that close no issue) are flagged for manual confirmation rather than guessed.

Re-requesting review — thanks again for catching this. 🙏

@Hareet
Hareet requested a review from sugat009 June 28, 2026 02:17
@Hareet Hareet self-assigned this Jun 28, 2026
@Hareet Hareet moved this from Todo to In Review in CHT Multi-Agent System (cht-agent) Jun 28, 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.

Re-reviewed at a0ae253b against the live cht-core API and the code. Strong fix: the distiller no longer aliases the PR number, the shared resolver and relink tool are clean, and 13 of 14 drafts are correctly relinked. One systemic gap I think is worth closing before merge (inline), the rest are notes and suggestions. All of it is open to discussion, please push back wherever I've read it wrong.

for (const ref of closingRefs) push(ref.number, 'closing-ref');

const titleIssue = parseTitleIssue(prTitle);
if (titleIssue !== null) push(titleIssue, 'title');

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): a title/body-derived number is trusted as an issue without confirming it isn't a PR

parseTitleIssue (:24) and this push(titleIssue, 'title') accept any positive integer; nothing checks that N is an issue rather than a pull request. So when a PR title is fix(#N): with N itself a PR (a follow-up PR) and closingIssuesReferences is empty, a PR number flows into issueNumber/issueUrl/id. Since this resolver is shared, the forward scraper reproduces it too (see the note on fetchLinkedIssues).

One way to close it: after resolving a title/body candidate, verify it's a real issue (gh api repos/<repo>/issues/N -> a pull_request field means PR, 404 means missing); if it's a PR, follow one hop to that PR's closingIssuesReferences, else flag. Keeping this file pure and putting the gh check in the callers that already shell out would preserve the scraper/relink symmetry. Open to other approaches, what's your read?

Comment thread src/scripts/scraper.ts

return Array.from(seen)
.map((issueNumber): LinkedIssue | null => {
function fetchLinkedIssues(refs: IssueRef[], repo: string): LinkedIssue[] {

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.

note: this is the path that lets the above reach the corpus

gh issue view <N> succeeds on a PR number (GitHub serves PRs through the issues endpoint), so a PR-number ref isn't dropped by the catch below. Just flagging where an is-it-an-issue check would naturally live.

Comment thread src/scripts/relink-issues.ts Outdated
function classify(ctx: FileCtx, online: boolean, exec: ExecFn): Classification {
const src = ctx.src;
if (!src) return unchanged(ctx.file, ctx.issueNumber); // old-convention file, not the alias bug
if (ctx.issueNumber === src.prNumber) return classifyAffected(ctx, src, online, exec);

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): the relink's "affected" test is narrower than the bug

classify treats a draft as affected only when issueNumber === src.prNumber. The 10399 draft has issueNumber 10182 != source_pr 10399, so it falls to classifySuspect, which passes it because issueNumber matches the filename token (:272) -> neither relinked nor flagged. Broadening this to "issueNumber doesn't resolve to a real issue" (the same gh check as above) would catch both the original alias and this second-order case, and a re-run picks up the stragglers. Does that seem right to you?

category: bug
domain: data-sync
domainFit: strong
issueNumber: 10182

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 issueNumber resolves to a PR, not an issue

10182 is a pull request (fix(#10183):); on GitHub PR 10399 closes nothing, and the real issue reads as 10183 ("Replication fails to filter out reports with needs_signoff set to false"), which matches this draft's summary. So this looks like the one draft the relink missed. After the resolver change it should re-stamp to 10183 (id/issueNumber/issueUrl). Does that match your read of it?

@@ -0,0 +1,83 @@
---
id: cht-core-10792

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.

note (non-blocking): id isn't unique here, and that's probably fine, just worth a decision

10793/10798/10799 all resolve to issue 10792, so all three carry id: cht-core-10792. Harmless today (nothing keys on id), but schema.json:62 and TEMPLATE.md:81 call id a "Unique identifier", which the corpus can't honor once multiple PRs fix one issue. Lightest fix is just softening that wording, plus making sure the #135 consumer dedups on issueNumber rather than id. A composite cht-core-<issue>-pr<pr> is the honest long-term shape if anything ever needs an id-keyed map, but that's a corpus-wide re-stamp and probably not worth it now. Curious which way you'd lean.

Comment thread src/scripts/relink-issues.ts Outdated
/** Single sidebar closing-ref is ground truth; use it even if the token disagrees. */
function resolveFromSidebar(authoritative: number, token: Token | null): Resolution {
const tokenMismatch = token ? token.issueNumber !== authoritative : undefined;
return relink(authoritative, 'gh', tokenMismatch);

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.

suggestion (non-blocking): cover the tokenMismatch=true path

This branch (sidebar wins over a disagreeing filename token) is the safety-critical override and the only one that sets the TOKEN-MISMATCH audit flag, but the spec only asserts tokenMismatch === false. A case where the sidebar closing-ref and the filename token disagree would lock it in.

let fm = block[2];
const repoName = repo.split('/')[1]; // medic/cht-core -> cht-core
const edits: Array<[RegExp, string]> = [
[/^id: cht-(?:core|interoperability)-\d+$/m, `id: ${repoName}-${newIssue}`],

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.

nitpick (non-blocking): not quite repo-agnostic despite the doc

The id/issueUrl match regexes hardcode owner medic and the two repo names, so a non-medic repo would throw rather than rewrite. Either generalize or tweak the "repo-agnostic" comment. (No CRLF / non-medic test exists either.)

Comment thread src/scripts/issue-linkage.ts Outdated
const raw: Array<{ number: number; url?: string }> = Array.isArray(meta.closingIssuesReferences)
? meta.closingIssuesReferences
: [];
return raw.filter(r => typeof r.url === 'string' && r.url.includes(`/${repo}/issues/`));

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.

nitpick (non-blocking): unanchored substring match

url.includes('/<repo>/issues/') also passes for a non-host occurrence (e.g. https://github.com/attacker/medic/cht-core/issues/1). Not exploitable on trusted GitHub data, but anchoring to the host would be tidier.

Hareet and others added 2 commits June 30, 2026 21:27
… — addresses #129 review

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

Hareet commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Thanks @sugat009 — all seven points addressed. Rather than patch just the 10399 case, we enumerated the full "a number could be an issue OR a PR" space and handled it systematically. All checks green (611 tests, coverage, SonarCloud).

Root cause

GitHub serves PRs through the issues endpoint, so gh issue view <pr> succeeds and a PR number passed as an issue. The only reliable disambiguator is the pull_request key on gh api repos/<repo>/issues/N — that's now the basis of a small shared module, gh-classify.ts, consumed by both the scraper (forward pipeline) and the one-off relink so they can't drift. issue-linkage.ts stays pure; all gh checks live in the callers, as you suggested.

Blocking

  1. Title/body number not verified as an issue — classifyNumber confirms a title/body candidate via the pull_request key; if it's a PR, resolveRealIssue follows it one hop to its closingIssuesReferences, else flags. Wired into scraper.fetchLinkedIssues and the relink. Closing-refs are trusted (issues-only); only title/body are verified.
  2. "Affected" too narrow — classify broadened from issueNumber === source_pr to "issueNumber doesn't resolve to a real issue" (PR or missing), keyed off the draft's own source_pr repo. Precedence: source-PR closing-refs first, then follow the stored issueNumber-PR.
  3. 10399 → 10182 (a PR) → real issue 10183 — resolved end-to-end and asserted in a regression test; it lands in the data-sync re-run.

Non-blocking

  1. Non-unique id — schema id description softened to note it isn't globally unique (several PRs can close one issue); consumers dedup on issueNumber (Context analysis agent doesn't load memory-pipeline drafts (wrong path + schema) #135). The relink surfaces every collision cluster (collidesWith) as that dedup worklist.
  2. tokenMismatch coverage — added; the 10399 test asserts tokenMismatch: true (gh sidebar overrides a disagreeing filename token, recorded for audit).
  3. Repo agnosticism — corrected the doc to state it handles medic cht-core/cht-interoperability (matching the regex), and every probe now uses the draft's own source_pr repo, so cht-interoperability drafts resolve against their own repo, not a hardcoded default.
  4. URL filter — sameRepoClosingRefs now anchors: startsWith('https://github.com/<repo>/issues/'), closing the github.com/attacker/medic/cht-core/issues/1 crafted-path bypass; body-URL refs pointing at another repo are dropped at the (pure) source too.

Extra cases the enumeration caught (beyond 10399)

  • Multi-issue PR (source or referenced) → flagged for manual choice, never guessed.
  • PR that closes no issue → flagged.
  • Transitive PR→PR chains / cycles → since closing-refs are issues-only, a PR-hop lands on an issue in one hop; we deliberately do not follow a hopped PR's title/body, so there's no recursion/cycle surface — deeper chains flag.
  • Cross-repo body URL (shared numbering) → dropped at the source; the pull_request check wouldn't have caught this (it's wrong-repo, not issue-vs-PR).
  • Transferred/converted issue → repository_url mismatch → treated as missing/flagged, not silently resolved to the redirect target.
  • Transient gh error (rate-limit/5xx) → fail-loud (ScraperError, cause preserved) in the scraper / flag in the relink — never silently drops a linkage, which would otherwise flip a filter-skip into a distill.
  • Idempotency — a relinked draft whose filename token is now stale stays unchanged on re-run (no re-flag loop).

Re-requesting review 🙏

@Hareet
Hareet requested a review from sugat009 July 1, 2026 03:32

@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.

Re-reviewed at a152324 against the live cht-core API, the code, and the tests. The code fix is excellent and fully closes the issue-vs-PR gap, including the nested 10399 case, with a direct unit test for it; the schema id wording, the tokenMismatch=true test gap, and the substring/doc nitpicks are all resolved. One data step remains: the relink wasn't re-applied, so the committed 10399 draft still carries a PR number (inline). Requesting changes just for that re-run.

* Returns issue:null with a reason for missing / no-issue / multi-issue.
* @throws {GhTransientError} on a transient gh failure.
*/
export function resolveRealIssue(repo: string, n: number, exec: ExecFn, cache?: ClassifyCache): ResolveResult {

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.

praise: clean resolution of the gap. Classifying via the pull_request key, following a PR to its sole closingIssuesReferences (one authoritative hop), and surfacing GhTransientError so a throttled lookup can't demote a real issue, all exactly right, and more robust than the review asked for.

* Resolve an affected draft's true issue: try the source PR's closing-refs first
* (most authoritative), then follow the stored issueNumber if it is itself a PR.
*/
function resolveAffectedIssue(src: SourcePr, issueNumber: number | undefined, gh: GhCtx): ResolveResult {

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.

note: verified this recovers the nested case. Trying the source PR first, then following the stored issueNumber when the source closes nothing, is what turns 10399 into 10183 (source 10399 has empty closing-refs, so it falls to issueNumber 10182 [a PR], whose closing issue is 10183). Multi-issue correctly short-circuits to a flag rather than guessing off issueNumber. Nicely done.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

Deep re-review at 58e1e0c. The data is now whole: all 14 data-sync drafts resolve to real issues (checked against GitHub closing-refs), the 10399 -> 10183 relink is correct, and the id-collision is now documented in schema.json with the #135 dedup contract. The resolution core (gh-classify / issue-linkage / the relink tool) is sound: frontmatter-only rewrites verified, regexes linear, fails closed. Approving. A few non-blocking refinements inline. The fail-loud gap in hydrateIssue is the one I'd most want your read on, here or as a follow-up. Nothing blocks on data grounds.

raw = exec('gh', ['api', `repos/${repo}/issues/${n}`]);
} catch (err) {
const text = errText(err);
if (/HTTP 404/i.test(text)) return null; // 404 only — never treat a transient "not found" as missing

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.

praise: The GhTransientError design here is the right backbone for this PR. Treating only HTTP 404 as missing and raising on everything else means a rate-limited lookup can never demote a real issue to missing, the exact failure that would silently mis-attribute a draft. resolveRealIssue's one-hop, single-closing-issue conservatism (0 or >1 both yield null) is nicely defensive too.

Comment thread src/scripts/scraper.ts
const parsed = JSON.parse(raw);
const comments: string[] = (parsed.comments ?? []).map((c: { body: string }) => c.body);
return { number: n, body: parsed.body ?? '', comments };
} catch {

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): hydrateIssue swallows every gh issue view failure into null, transient (rate-limit / 5xx) as well as a real 404. That undoes the fail-loud guarantee the rest of the module works for: resolveRef (L87) trusts closing-ref numbers directly without a classify call, so for the highest-authority source this is the only gh touch and has no transient protection. Under bulk-run throttling, a closing-ref issue can be dropped silently and a lower-authority body ref promoted into linkedIssues[0], the same silent mis-attribution this PR exists to prevent. resolveRef's own docstring (L80-84) even promises transient errors propagate rather than drop a linkage. Could hydrateIssue mirror fetchIssueRecord, return null only on HTTP 404 and throw GhTransientError otherwise? Open to whether that's worth doing here or as a follow-up, since the pattern predates this PR.


function readCtx(file: string): FileCtx {
const content = fs.readFileSync(file, 'utf8');
const fm = matter(content).data as Record<string, unknown>;

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): matter(content) isn't guarded, so a single draft with malformed YAML frontmatter throws out of planFile and aborts the whole relinkIssues run with a raw stack and no report. It fails closed (all planning precedes applyRelinks, so nothing is half-written), which is the important part, but as a corpus-maintenance tool it'd be friendlier to catch per-file and flag it as unparseable alongside the other flagged files. Minor, since real drafts are schema-validated on the way in.

Comment thread src/scripts/scraper.ts
/** Resolve + dedup + hydrate one ref; marks `seen` only on a successful new issue. */
function linkOneRef(ref: IssueRef, repo: string, cache: ClassifyCache, seen: Set<number>): LinkedIssue | null {
const issueNum = resolveRef(ref, repo, cache);
if (issueNum === null || seen.has(issueNum)) return null;

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.

suggestion (non-blocking): This resolveRef -> null -> drop line is the safety valve that stops a PR number being recorded as the issue, but it isn't pinned by a scrapePR-level test. The PR-following tests cover the single-closing-issue happy path; none drives a title/body ref whose resolveRealIssue returns null (multi-issue / no-issue / 404). If this regressed to ... ?? ref.number, the original PR-as-issue bug returns and every scraper test still passes (it's only unit-tested in gh-classify.spec). A scrapePR case where a title ref points at a PR closing 0 or >1 issues, asserting the ref is dropped, would lock it down.

*/

/** The source that contributed a linked-issue number, by descending authority. */
export type IssueSource = 'closing-ref' | 'title' | 'body';

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.

thought (non-blocking): IssueSource is a nice model, but the resolved source is used only for ordering and isn't persisted onto the draft. In the current corpus 4 of 14 links are title-derived (the PR closes nothing per GitHub's closing-refs, so the number comes from the fix(#N) title, the exact error-prone signal this PR fixes). They all resolve to real issues here, but they're indistinguishable from authoritative links once written. Persisting a linkSource/confidence field, or at least flagging title-only links, would let a human audit the low-confidence ones. Future-auditing thought, not a blocker.

alexosugo added a commit that referenced this pull request Jul 8, 2026
- Flip issue-resolution precedence to match shipped issue-linkage.ts
  (closingIssuesReferences > title > body, descending authority)
- Fix PR 10623 hallucination facts: only user-contact.service.ts is
  invented; message.pipe.ts/reducers/tasks.ts are real
- Narrow #129 open gaps to deduplication (title-parse + forward-write
  path + nested-chain case already shipped and tested)
- Reframe roadmap as tracking #138; rewrite rank 4 to the shipped
  require-an-issue-or-skip approach
- Relabel 60/107 as the four fully-reviewed domains, not corpus-wide
- Appendix: #10036 x4, subDomain-schema draft caveat, verbatim 8675/8843
  titles, CouchDB 9960/10014 close different issues
@Hareet
Hareet merged commit fdf4af2 into main Jul 9, 2026
4 checks passed
@Hareet
Hareet deleted the memory/promote-data-sync branch July 9, 2026 15:23
alexosugo pushed a commit that referenced this pull request Jul 10, 2026
… — addresses #129 review

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alexosugo added a commit that referenced this pull request Jul 10, 2026
R2: open-review-pr rejects a draft whose issueNumber aliases its own
source PR number, or whose filename slug contradicts its frontmatter
issueNumber (src/scripts/dedup.ts ciGuardReason).

R3: a cross-domain dedupeByIssueId pass collapses backport cherry-picks
and multi-PR epics that resolve to the same issue id into one canonical
draft (lowest source PR number), tagging it with source_prs[]. Adds the
source_prs field to agent-memory/schema.json.

Builds on the R1 issue-resolution fix already on this branch (cherry-picked
from PR #129's gh-classify.ts/issue-linkage.ts), which makes issueNumber a
stable, non-aliased key these checks can trust.
Hareet added a commit that referenced this pull request Jul 17, 2026
…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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, authentication)

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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, 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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, configuration)

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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, infrastructure)

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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, forms-and-reports)

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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ly, tasks-and-targets)

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>
Hareet added a commit that referenced this pull request Aug 5, 2026
…peline for review (#130)

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

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

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): review fixes (configuration)

Per sugat009's review on #130: the 4 mislinked identity keys were fixed
by the relink commit; this follow-up verifies and polishes the corpus.
All 10 PR-to-issue mappings verified against the live cht-core API
(0 mismatches) — including 10555, whose issueNumber 10556 is a genuine
body closing ref ("Closes #10556", the pt-BR translations request),
not a distiller artifact.

Also: scrub reviewer/process narrative and classifier seed references
from 7 files (technical content kept, attribution removed), and add the
optional source_prs schema definition (identical to the other seeders).
No duplicate issueNumbers; validate-schema 74/74.

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

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

Every claim in these ten drafts was checked against the medic/cht-core commit it
was distilled from, using word-bounded `git grep`, `git diff-tree --name-status`
and `git ls-tree`. 124 claims verified true; the corrections below are the ones
the source contradicted. Reviewer-flagged items are marked [R].

10604 — the draft described the wrong bug and the wrong fix:
- [R] the view is `medic-client/doc_by_type`; `docs_by_type` exists nowhere
  (0 hits at 06d2e3abe and on master; the real view has 22).
- [R] no migration ever changed a view path. #10255 (de02d8421) removed the
  `translations` special case from the doc_by_type map, so the view stopped
  emitting `['translations', doc.enabled]` and its `{code,name}` value; that PR
  updated two admin controllers but missed the languages service.
- the fix did not repoint at a "current view path" — it replaced the query with
  an allDocs range scan over the `messages-` prefix and projects off `row.doc`;
  the lodash/core import was dropped.
- the spec was ADDED, not updated (diff-tree says A) — the service had no spec.
- dropped `related_workflows: [data-migration]`; no migration is involved.

8722 — [R] `add-branding-doc.js` was DELETED and replaced by
add-cht-branding-doc.js, not "added alongside, preserving the original"; the
replacement upserts and only overwrites attachments whose digest still matches
the old Medic assets. Removed the deleted file from entities, marked A/D in
Related Files, and dropped the anachronistic `ui-extensions` workflow tag (that
workstream first appears 2026-03-30; this anchor is 2023-11-28).

11021 — [R] `updateServiceWorker()` is defined and exported in config-watcher.js
itself (line 140/208), not imported from ui-extension.js, which exports only
getScript/getAllProperties. The documented test command was also broken:
`@medic/environment` exits(1) without COUCH_URL and db stubbing needs
UNIT_TEST_ENV=1, so the repo script `npm run unit-api` is the correct form.
The `ui-extension:` literal is deliberately left alone — that PR postdates the
local checkout and master has no PREFIXES.UI_EXTENSION, so either spelling could
be right.

9727 — `language.service.ts` does not read the rtl property off a doc; it holds
an in-memory registry. translation-loader.provider.ts reads `doc.rtl` and calls
setRtlLanguage. Dropped the anachronistic ui-extensions tag.

10278 — one line was added, in the attachment path only; the load path was
untouched. Also spelled out the two real guard forms (`res && res.resources` in
admin JS, `res?.resources ?? {}` in webapp TS).

10198 — the controller scaffolds one empty `resources` object and guards the
favicon/icon assignments; template safety comes from new ng-if attributes, not
from scaffolded keys.

9696/9727 — stripped classifier scaffolding from 9696's Domain Rationale and
cross-linked the RTL/translations pair in related_issues, both as requested in
review.

Verification: 124 claims confirmed against source, 0 contradicted. The
remaining unverifiable claims are confined to 11021 and 11057, whose source PR
numbers do not resolve to any commit in cht-core; nothing in either draft 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): drop the service-worker rebuild draft — the change is not in cht-core

This draft describes the config-watcher gaining a UI-extension doc-id branch that
calls updateServiceWorker(), and presents it as completed and tested. That code
is not in cht-core.

    git grep -n "startsWith" origin/master -- api/src/services/config-watcher.js
      :167  change.id.startsWith(PREFIXES.TRANSLATIONS)
      :171  change.id.startsWith('form:')

There is no UI-extension branch, and no commit anywhere in the repository
references the issue the draft claims to close. PREFIXES.UI_EXTENSION does now
exist in shared-libs/constants, so the work may be in flight, but nothing has
landed.

This is the same class as a draft distilled from an issue rather than a merged
PR: a memory asserting behaviour the codebase does not have is worse than no
memory, because an agent consuming the corpus cannot tell the difference. Better
re-distilled once the change merges.

Removing it also retires the one place on this branch where a correction had to
be guessed — the draft's 'ui-extension:' literal could not be checked, since
master carries no PREFIXES.UI_EXTENSION usage to compare against.

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

* fix(#136): time-scope stale master claim, record epic provenance, wording fixes (configuration)

- 10604: the doc_by_type consumer count is scoped to the fix's era (18 files);
  #10822 (57ea9228b, 2026-07-01) has since moved consumers off the view and
  narrowed its index to form/user-settings — present-tense master claim was stale
- 11057: provenance is real, not doubtful — PR #11057 merged into the
  10224-ui-extensions feature branch; its merge commit IS source_sha (46c8f7c8e),
  unreachable from master only because the epic squashed via #11050 (180c29ecf).
  Added source_prs [#11057, #11050] and a Related Issues note so anchors resolve
  from a master clone
- 10278: 'browserify-compatibility const was documented' -> the reviewer-suggested
  ?. one-liner broke the admin browserify build (review thread), explaining the
  divergent admin/webapp guard spellings
- 10555: AI disclosure is a PR-description section, not a file comment; strip
  triage vocabulary ('catch-all') from the Domain Rationale
- 8722: complete (added)/(deleted) annotations on the logo asset Related Files

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

* fix(#136): resolve round-3 review + the self-contradiction class behind it (configuration)

Sugat's two residuals and two nitpicks, plus the same class found
systematically rather than only where he pointed.

10198 — the draft contradicted itself in four places on the one
mechanism the grounding pass corrected. Solution says template safety
comes from the new ng-if attributes 'not from scaffolded keys', while
summary, Code Patterns and Design Choices all still credited the
scaffolding. Only Testing was flagged in review; a coherence pass over
the whole file found the rest. All four now agree: reads are guarded so
missing images stay undefined, the template hides them with ng-if, and
the single scaffolded 'resources' object exists because the submit path
writes into it. Also drops the stale 'One related case is not covered'
artifact — the merged spec covers both the empty-doc and null-resources
cases.

10278 — resource-icons.service.ts and resource-icon.service.spec.ts were
replaced by custom-resource.service.* in #11050 (180c29ecf), so both were
named in the present tense for paths that no longer exist. Time-scoped in
10604's style, with a paths note covering every mention rather than the
four lines review listed.

11057 — source_prs dropped. #11050 resolves #10224, not this draft's
#10908, so it never satisfied the field's 'All PR refs that resolve to
this issue'. Nothing is lost: the API resolver anchors this draft by
hopping from #11057's base branch, and the epic provenance was already
prose in Related Issues.

lastUpdated bumped on 10198, 9696, 9727 (review) and 9407 (found by the
new stale-timestamp check, which compares the stamp to the file's last
commit rather than eyeballing which drafts moved).

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

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

* fix(#136): timestamp and entities corrections from the independent audit (configuration)

- 9407: the stamp was set to the date of its last CONTENT commit
  (2026-07-16), but setting it is itself an edit, so the branch's own
  stale-timestamp check then failed the file. The rule that actually holds
  is 'touch the file, stamp it today'.
- 10278: my (replaced by custom-resource.service.ts in #11050) suffix on
  an entities entry made a machine-readable path non-bare, which any
  downstream consumer matching on paths would miss. Reverted to the bare
  path; the caveat already lives in the Code Patterns note and the
  Related Files annotation, where prose belongs.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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 added a commit that referenced this pull request Aug 11, 2026
…y-pipeline for review (#123)

* chore(memory): promote strong-fit tasks-and-targets drafts for review

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

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 (tasks-and-targets)

Per sugat009's review on #123: fix the hallucinated identity on 9232 —
its stored key was medic/care-teams#137 mis-stamped as cht-core; the
memory is now keyed cht-core-9231 (PR title scope) with the care-teams
Related Issues lines dropped. Collapse the 3 clusters to one memory per
issue with source_prs[]: 9231 (9232+9282+9317 sequential facets), 9431
(9486 + 4.13.x sibling 9549), and 9552 (9553+9555+9569+9570 — the two
distinct sub-fixes attributed separately, backport lines noted).

Drop 8838 (closes no tracked issue — skip-and-flag policy). Cross-domain
dedup: 9099 (#6543 facet) moves to the authentication canonical; 10432
(#10344) to the contacts corpus's existing memory; 9975 (#9974) to the
forms corpus's existing memory — refs recorded there in touch-ups.

Forced fits re-annotated weak (10390 datasource, 8932 cross-component,
10786 telemetry pipeline). Process narrative scrubbed from 11 files
(named reviewers, CI-status and AI-change chronology).

All 35 mappings verified (live cht-core API + the reviewer's own
closingIssuesReferences audit for rate-limited cluster members);
validate-schema 89/89; no duplicate issueNumbers.

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

* fix(#136): ground tasks-and-targets drafts against cht-core source

Every factual claim in the twenty-five drafts on this branch was checked against
the cht-core commit it was distilled from. 319 claims confirmed; 71 corrections
applied. Each correction was re-checked by a second pass instructed to refute it.

Two drafts on this branch contradicted each other, and the source settled both:

BACKPORT LINE. 9553 said the fix "was backported to the 4.1.x line (PR #9555)"
while its sibling 9486 said 4.13.x. The backport commit c8a7f13ad is titled
"...for 4.13.x (#9555)" and is an ancestor of origin/4.13.x but not of
origin/4.1.x. 9553 corrected; 9486 was already right.

TASK ORDERING. 10362 said tasks are "ordered by due date and then priority";
9980 said priority descending with due date as tie-break. The shared comparator
in shared-libs/task-utils runs a priority cascade first and only reaches
compareDates() when priorities are invalid or equal, so 9980 is right and 10362
was inverted. Corrected in 10362.

10362 was wrong in three further ways: the notifications are Android device
notifications delivered through globalThis.medicmobile_android, not in-app ones,
and are inert in a browser; the new service reads no NgRx state, only
RulesEngineService.contactsMarkedAsDirty; and the ordering comparator was not new
logic but an extraction of an existing private function out of the tasks reducer.

Also corrected: 10772 described its e2e spec as added when the commit modified it
(only the target config was added), plus prior-state overstatements, A/M status
confusions and telemetry key inexactness across the branch.

Left unedited deliberately: 10390's cht-datasource module names, which conflict
with 10423's but cannot be checked because its own commit is unrecoverable; and
10436's Mocha-harness attribution, for the same reason. Six proposed corrections
did not survive re-checking. 108 claims remain unverifiable, chiefly on drafts
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): bump lastUpdated on all 25 drafts (tasks-and-targets)

Every draft in the batch was edited by the relink, dedup and grounding
commits without its stamp moving, so all 25 failed the stale-timestamp
check. Stamped today rather than with each file's last content-change
date: the stamp edit is itself a commit, so a content date fails on the
very commit that sets it - the rule learned twice on the configuration
and messaging branches.

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

* fix(#136): time-scope four drafts whose epic renamed their files (tasks-and-targets)

10324, 10371, 10436 and 10507 all merged into the 10140_previous-month-targets
feature branch and reached master only in that epic's squash, #10423
(622c625427). The epic reshaped them on the way:

- the e2e directory was renamed analytics/ -> targets/, so the
  tests/e2e/default/analytics/analytics.wdio-spec.js these drafts name is
  tests/e2e/default/targets/analytics.wdio-spec.js on master;
- webapp/src/ts/libs/config.ts, created by 10507 and renamed by 10436, does
  not exist on master at all - only .mocharc.js survives of the mocha
  harness, and the subtitle handling now lives in rules-engine.service.ts;
- RulesEngineService.fetchTargets() is spelled as a bare fetchTargets() on
  the service (rules-engine.service.ts:501) and really did gain the
  reporting-period argument this draft describes.

Every path is accurate for its own PR, which is why grounding at the anchor
passed them; they are wrong only as directions for a reader looking at
master today. Each now says so, with the landed location where there is
one. This is the drift class the probe was built for, arriving in bulk
because five drafts share one epic.

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

* fix(#136): twelve self-contradictions across nine drafts (tasks-and-targets)

Same class the reviewer identified on #120 and #130: the grounding pass
corrected the sections that assert mechanism against code and left summary
and Design Choices asserting what it had just disproved. Every pair below
was found by check-coherence over two passes and then verified against
cht-core; in each case the corrected body was right and the stale side was
rewritten to match it.

- 10362: summary said the service is 'wired into task state' and Design
  Choices said it consumes task state, while Code Patterns says it reads no
  NgRx state - it subscribes to contactsMarkedAsDirty and fetches docs.
- 10480: summary said the counter mirrors the unread-count pattern; Code
  Patterns says the PR generalises that flow (setUnreadCount became
  setBubbleCounter). Testing credited the rules-engine integration test
  with 'the count computation'; the engine contributes the showTask
  predicate (index.js:117) and the count is computed in the webapp.
- 9232: summary said the filter appears only for multi-facility users and
  single-facility users see no change; Solution says there is no
  facility-count term, so everyone with a facility list gets it.
- 9553: summary said the fix reconciles state against the configuration;
  the body says isStale only checks the blob has targets and aggregate keys
  and never reads the configured targets. Solution also said turnover
  re-scopes/resets emissions; Code Patterns says they are preserved.
- 9705: Design Choices claimed the fix throws on WRITE errors; the change is
  to a read catch-all, and the file's one bulkDocs still only console.errors.
- 10623: Design Choices said the filters were reused rather than built;
  overdue-filter and task-type-filter are new components, both on master.
- 8932: summary called the flashed text 'empty-state messages'; Root Cause
  shows they are end-of-list messages gated on has-items being true.
- 10371: Design Choices claimed a new reusable telemetry test util;
  tests/utils/telemetry.js pre-existed and was modified.
- 10324: summary said targets analytics had no way to review prior periods;
  Problem says target-aggregates already had that filter since #9317.

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

* fix(#136): drift and two wrong file statuses (tasks-and-targets)

Real defects:
- 9553 called target-accuracy.wdio-spec.js an added e2e spec; it was
  modified. Its migrateStaleState sentence is also attributed to #9553,
  but the symbol enters in the #9569/#9570 follow-up - it is absent at
  dc47c51e4 and present on master at target-state.js:124.
- 9980 called tasks.spec.ts added; diff-tree says modified.

Drift - true at the anchor, gone from master, now time-scoped:
- 10362's shared-libs/task-utils/test/order-by-due-date-and-priority.js,
  removed by #10701.
- 9232's analytics-target-aggregates-sidebar-filter component and spec,
  folded into the shared analytics sidebar filter by the same #10140 epic
  that reshaped four other drafts in this batch; its modules.module.ts,
  dissolved by the Angular 19 standalone migration (#9759); and the
  can_view_old_filter_and_search / can_view_old_action_bar permissions,
  both retired from master.

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

* fix(#136): keep 9232 entities a bare path (tasks-and-targets)

My drift annotation landed on the entities entry rather than the Related
Files line - replace(...,1) hits the frontmatter occurrence first - and the
colon inside it made YAML parse the item as a map, so validate-schema went
88/1. Reverted to the bare path, annotation moved to Related Files where
prose belongs. Same mistake as 10278 on the configuration branch; entities
is machine-readable and any downstream consumer matching on paths would
miss an annotated one.

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

* fix(#136): the two contradictions that survived three passes (tasks-and-targets)

Both had resisted an earlier fix because I corrected one statement and left
a third one standing - the same one-section-at-a-time failure this whole
exercise is about.

9232 made three incompatible claims about what gates the filter UI. The
summary said multi-facility users only; I corrected it to 'has a facility
list at all', which was also wrong; Code Patterns still said 'only when
facility_id resolves to multiple facilities'. What Solution actually
documents is canDisplayFilterButton() gating on !isAdmin plus the legacy
permissions being absent, with facility count controlling only the radio
group inside the sidebar. All four statements now say that.

9553 said the interval-turnover migration fires 'when the persisted
reporting interval no longer matches the current CalendarInterval' while
Code Patterns says it reuses the shape check rather than comparing
intervals. My previous edit fixed the emissions half of that sentence and
left the trigger clause. Rewritten: load() runs the shape check on every
hydration.

Also time-scoped the two legacy permission constants where they are quoted,
both retired from master.

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

* fix(#136): cross-reference glosses and PR labels (tasks-and-targets)

From the online audit, which this branch had never had - an earlier attempt
rate-limited 23 of 25 drafts, and record caching (baeb04b) is what made a
complete 25-draft scan fit the 60/hour anonymous budget. It finished with
0 unverified.

Blocking:
- 9486 cited #9432 as 'recalculate tasks/targets automatically on document
  or state changes (debounced)'. #9432 is 'Merge ensureTaskFreshness and
  ensureTargetFreshness into single event' - a performance issue about
  folding two 120-second background refreshes into one, not about
  triggering on document changes.
- 9718 cited #9486 as an issue describing 'prior work improving
  recalculation'. #9486 is a PR, 'feat(#9431): always aggregate and store
  targets'. Now labelled and titled.

Also: #6209's gloss was fair but vague enough to trip the weak-overlap
warning, so it now carries the real title; and 9232's #9267/#9283/#9305 are
PRs cited as issues, now labelled PR #N.

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

* fix(#136): five more partial-fix mismatches (tasks-and-targets)

Three of these five exist because an earlier commit in this same PR
corrected one section and left a sibling asserting the old thing - the
failure mode this whole exercise is about, committed by the person fixing
it, twice now. Recording that plainly:

- 9232: my summary rewrite said the facility radio group is the only
  element gated on facility count; Design Choices, Solution, Code Patterns
  and Testing all also list the per-aggregate facility indicator.
- 9705: I corrected Design Choices to the read-side catch-all and left the
  summary saying errors were swallowed 'while saving' with 'failed writes'.
- 9553: I corrected Testing to 'modified not created' for the e2e
  target-accuracy spec and left Solution saying the spec was added.

Two were pre-existing:
- 10436's summary attributed the wrong-subtitle defect to both analytics
  pages; Problem confines it to target-aggregates.
- 9980's Design Choices claimed changes were 'confined to the webapp' while
  Solution lists config/default/tasks.js, which is not in the webapp.

Coherence over three passes went 5/2/2 before these fixes to 2/3/0 after
the previous round, with 9232 and 9553 dropping from every pass to one -
the sampling behaviour is why the gate is three passes and not one.

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

* fix(#136): two drafts called modified specs 'added' (tasks-and-targets)

Both are real imprecision the exhaustive pass caught, not tool artifacts:

- 10390 lists five mocha specs as added for the new datasource module.
  Three were; test/qualifier.spec.ts and test/index.spec.ts already existed
  and PR #10390 extended them.
- 10786 says it 'added sentinel replications.spec.js and mocha
  purger.spec.js coverage'. The coverage was added, but both spec files
  pre-existed and were modified. Reworded to 'extended the existing'.

Small, but the whole point of the corpus is that a reader can trust a file
list, and 'added' versus 'extended' is exactly the sort of detail an agent
would act on.

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

* fix(#136): three contradictions the x3 coherence gate surfaced (tasks-and-targets)

9705 recurred in all three passes, which is the signal that separates a real
finding from sampling noise - and it was mine. My earlier summary said the
catch-all 'swallowed every error into a synthesised default document'. The
code at the anchor's parent is 'if (err.status === 404)': only a 404 produced
a fresh doc, every other rejection fell through returning nothing. So the
failure mode was worse than the summary claimed - neither a document nor an
error - and the Root Cause section had it right all along. Summary and Design
Choices now match it.

9486 said aggregation was 'only triggered when the user navigated to certain
pages', which its own Root Cause refutes: a mark-contacts-dirty change-feed
hook already existed pre-fix (confirmed at dc3ef42ab^). What was missing was
aggregation and persistence, not the trigger. Reworded.

9553 named handleIntervalTurnover in three places with no temporal
qualifier; #9714 removed interval turnover from master altogether, so the
function is gone. Time-scoped.

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

* fix(#136): disambiguate which 9486 file got the change subscription

Code Patterns listed both key files and then appended '(change subscription
+ debounce)', which reads as applying to both. The Solution says db-sync
changed only to make inProgressSync awaitable, and the diff agrees - three
lines, no subscription. Reordered so each file carries its own parenthetical.

Found by one coherence pass of three. By the protocol that is sampling noise
rather than a robust finding, but a single-pass finding was real once before
on this branch (9022), so they get read rather than dismissed. The other
single-pass finding this round was a genuine model error: its own rationale
said 'the first is not a contradiction of the second' and it reported the
pair anyway - the documented limit of a gate that proves quotes exist, not
that they conflict.

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

* fix(#136): an off-by-one and a wrong file status (tasks-and-targets)

Both single-pass findings, both real - which is why single-pass findings get
read here rather than written off as sampling noise.

8772's summary says the short-circuit fires 'when the key count exceeds
500'. The guard is 'if (!params?.keys || params.keys.length <
MAX_QUERY_KEYS) return' with MAX_QUERY_KEYS = 500, so it fires at 500 or
more, not above 500. A reader implementing against this would put the
boundary in the wrong place.

10423 calls shared-libs/cht-datasource/test/target.spec.ts a modified
datasource target module; diff-tree at 622c625427 reports A.

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

* fix(#136): 'Added/updated' hid four added files, and 'future' hid today

10423 wrote 'Added/updated mocha unit tests for ...' over a list of four
files that diff-tree reports as A, A, A, A. The hedge made the extractor
guess per file and made a reader unable to tell which. All four were added,
so the sentence now says so.

10480's Design Choices says the bubble counts 'Overdue' and due 'Today'
tasks, while the Solution said 'tasks due in the future are intentionally
excluded' - and a task due later today is in the future. Reworded to 'due
after today', with the due-today case stated explicitly.

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

* fix(#136): twelve more claims the exhaustive pass caught (tasks-and-targets)

Repeated ground and coherence passes over the same twenty-five drafts, on
the standing rule that recurrence proves a finding real while a single
appearance proves nothing. Every item below was checked against cht-core
or the issue text, not reasoned about.

Wrong facts:

- 10362 named the comparator `order-by-due-date-and-priority` in two
  sections. That is its test file; the export is orderByDueDateAndPriority.
- 11142 put FreetextFilterComponent in the tasks module. It lives in
  components/filters/freetext-filter/ and the Tasks page reuses it.
- 10436's Testing called two page objects and both e2e helpers new;
  diff-tree says both page objects were modified and only
  targets-helper-functions.js was added.
- 10623's Code Patterns credited reports and messages with dropping the
  duplicated lineage filter and omitted tasks.component.ts, which dropped
  its own removeUserFacility call in the same PR.

Summaries left behind by earlier corrections, the class this branch is
supposed to be closing:

- 9232 called a regression a missing feature. #9231 says multi-facility
  users could not view aggregate targets at all in 4.9.0.
- 10480 said the unread-count flow was generalised to carry the task
  count. setBubbleCounter still carries only reports and messages;
  getBubbleCounter spreads those and adds a task count off another slice.
- 8772 stated the short-circuit condition and then its inverse in the same
  parenthetical. Both true, unreadable together, and two passes read it as
  a contradiction.

Tense, where a file did not survive the 10140 epic:

- 10507 called libs/config.ts a helper it introduced and updated at once,
  and gave it a present-tense home the same draft denies.
- 10436 named it twice more unscoped.
- 10390 had no "paths are as of this PR" banner at all, despite ten of the
  twenty-one files its PR touched being target-interval.* names that exist
  nowhere in cht-core. It gets the banner; the vocabulary rewrite belongs
  to the data-access work.
- 10371's Code Patterns implied one component records both open and
  selection telemetry. analytics-filter.component.ts records only :open.

10480 also now says why a task due today counts: the due date is parsed
date-only, so it sits at midnight, and #3943 asks for exactly overdue and
due-today.

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

* fix(#136): three more, all found by repeating the same passes

- 10423's Testing listed eight test files under one "Added". Six were
  added; the two e2e WDIO specs already existed and were modified. Both
  ground passes agreed on this one.
- 10324's Testing opened the same way and is worse: its PR adds no test
  file at all. Every spec named was modified, and the sidebar-filter spec
  was renamed rather than created.
- 9553's summary blamed "a CHT upgrade that changes target configuration".
  Root Cause blames the #9486 persisted-shape change and the code agrees —
  isStale is `(state) => !state || !state.targets || !state.aggregate`, a
  shape check that never reads configuration.

added-versus-modified is now five of the findings on this branch. It is
caught only when extraction samples the sentence, because deciding it
means scoping "Added X, Y and Z" across a coordinated list. A regex
attempt at that flagged 126 of 164 mentions, including one sentence
reading "(modified, not created)", so it was dropped in favour of running
more passes.

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

* fix(#136): bump lastUpdated on the three drafts edited today

verify-drafts compares lastUpdated against the file mtime; the previous
commit landed after the date rolled over.

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

* fix(#136): two the fourth round of passes surfaced

- 10362's Domain Rationale said the implementation "centers on the task
  pipeline (new task-notifications service, tasks reducer, ...)" while its
  own Solution says the reducer is not consumed by the service and was
  only changed to import a comparator. The reducer comes out of the list.
  Same class as the rest of this branch: an interpretive section left
  asserting what the grounding pass had already disproved.
- 9232 wrote "added new translation keys
  (api/resources/translations/messages-en.properties)". The keys are new;
  the file is not, and the parenthetical reads as though it were.

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

* fix(#136): 9553 had interval turnover reading the wrong interval

Solution said the migrated emissions are rewrapped "so
handleIntervalTurnover can read them against the active interval", while
Design Choices said the same function writes the PREVIOUS interval's
target doc.

Design Choices is right. handleIntervalTurnover returns early when
stateCalculatedAt falls inside the current interval, and otherwise
aggregates against calendarInterval.getInterval(monthStartDate,
stateCalculatedAt) — the interval the stale state belongs to — and stores
that as the target doc. Reading against the active interval is the one
thing it never does.

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

* fix(#136): the 9553 interval fix introduced two unscoped anchor-era symbols

e1d0df4 corrected which interval handleIntervalTurnover reads, and in
doing so named calendarInterval.getInterval and stateCalculatedAt — both
real at the anchor, both gone from master because #9718 (also in this
batch) removed the interval-turnover mechanism entirely. The drift check
flagged them in three of the last four passes.

One scoping clause covers the sentence and points the reader at the 9718
draft, which documents the removal.

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

* fix(#136): the five items from review 4845979803 (tasks-and-targets)

Each was re-derived from the PRs' own diffs and cht-core master before editing,
not taken on the reviewer's word. All five checked out.

9553 - the symptom was inverted. Issue #9552 reports a crash, not wrong numbers:
`Object.keys(state.targets)` in aggregateStoredTargetEmissions throws
`TypeError: Cannot convert undefined or null to object` because a pre-#9486 blob
is a bare targets map. Interval detection was never the defect --
handleIntervalTurnover already did `moment(stateCalculatedAt).isBetween(...)` and
`calendarInterval.getInterval(...)` at fe795fb^, and #9569 added no interval logic
at all (+1 line in rules-state-store.js, +9/-4 in target-state.js, rest tests).
Title, summary, Problem, Root Cause, Solution, Testing and Related Issues now all
tell the crash story; the e2e case is named ('should handle old format of the
rules-state-store') instead of being called a configuration change.

10436 - the harness attribution was backwards. This PR *removed* the mocha pieces
#10507 had added: config.spec.ts +0/-103, tsconfig.mocha.json +0/-9, .mocharc.js
+0/-2, libs/config.ts +0/-26, all deletions. webapp/tsconfig.spec.json is the
webapp-root karma tsconfig, is on master, and only gained sinon-chai types here.
`getValueFromFunction` was deleted outright (zero-hit on master), not folded --
its role is now getReportingMonth in rules-engine.service.ts, which this PR added.

10362 - `orderByDueDateAndPriority` is not a task-utils export on master.
task-utils exports only setTaskState; the comparator is at reducers/tasks.ts:15
and the service imports it from @mm-reducers/tasks. #10701 moved it back, its
description saying task-utils is for report SMS tasks, not rules-engine tasks.
The draft now records the round trip and names master's location.

10324 - the radio labels are both wrong and the contrast is imaginary. Both
filters render `targets.this_month.subtitle` / `targets.last_month.subtitle` =
"This month" / "Last month"; "Previous month" is zero-hit in messages-en and is
only the ReportingPeriod.PREVIOUS enum value.

10371 - the telemetry segment is `:reporting-period`, matching
collectFilterSelectionTelemetry('reporting-period') and the key its own Solution
already quoted.

Two more on these same drafts, from sweeping the class rather than the instance.
10371 carried the identical epic-rename misattribution filed only against 10324:
the e2e analytics/ -> targets/ rename was #10480 (bed454652) on master, not the
#10423 squash. And 10324 credited itself with the sidebar's `telemetryKey` input,
which it never touches -- #10371 added it; 10324 added userFacilities and
showFacilityFilter.

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

* fix(#136): name which PR deleted libs/config.ts, in the draft that added it

10436's correction leaves 10507 as the only other draft describing
webapp/src/ts/libs/config.ts, and it attributed the file's disappearance to "the
epic" generically. It was #10436 specifically, later in the same epic, which also
removed the two mocha pieces 10507 lists in Related Files. Naming the deleting PR
makes the pair agree and gives a reader one hop to the whole story.

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

* fix(#136): four contradictions the gate found outside the review's five

Re-running the full gate over the reworked branch surfaced these on drafts the
round-4 review did not name. Each adjudicated against cht-core before rewriting.

9486 - three passes, because the first two fixes were themselves wrong. The
summary said targets were computed "only ... when visiting specific pages", which
its own Root Cause contradicts with two 120s ensure-freshness debounces
(`ENSURE_FRESHNESS_SECS = 120` at dc3ef42ab^, used twice). Correcting that to "a
120s debounce" then contradicted the same line's "two separate" -- and correcting
*that* left Problem claiming nothing ran on incoming changes while Root Cause said
the hook called updateEmissionsFor per change. At dc3ef42ab^ that function only
does `rulesStateStore.markDirty(contactIds)`: it invalidates, it does not
recompute. Problem now says exactly that.

9705 - the Problem opened by locating the swallowed error on the write, while the
summary, the next sentence, Root Cause, Solution and Design Choices all locate it
on the read that precedes the write. The read is right; the opener is now neutral
about which call failed.

10390 - Domain Rationale said "all changed files are shared-libs/cht-datasource
plus one API controller", but the PR also changes api/src/routing.js, adds
integration tests under the top-level tests/integration/ tree, and touches
.mocharc.js and package.json. Reworded to state the scope without under-counting.

8932 - the title called the flashed strings "empty states" while Root Cause shows
both are gated on the has-items flag being true (`hasTasks`, `hasContacts`), which
makes them end-of-list messages, the opposite of an empty state. Title and the two
tags follow.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Hareet added a commit that referenced this pull request Aug 18, 2026
`8034` was never part of #132. The original promotion (`aa61c5b`) left it
untouched — `git diff --name-status main...aa61c5b` does not list it — and my
August sweep of "landed drafts" pulled it into the diff. Reverted to `main`, so
the PR's scope is again what it was.

The reason is not only scope. `contacts/8034` is the likelier of the two
duplicates to be deleted outright, and polishing it is work spent on a file that
should probably go:

  contacts/8034   hand-authored  2026-06-04  a0108f7  chore(#73): categorize 10
                  closed cht-core issues in contacts domain (#79)
                  domain: contacts, subDomain: replication
                  no source_pr / source_sha / domainFit / confidence
  data-sync/9593  machine-distilled  2026-06-23  chore(memory): promote
                  strong-fit data-sync drafts (#129)
                  domain: data-sync, domainFit: strong

Not a stale artefact of a domain reorg, then — two independent production paths
three weeks apart, both keyed to cht-core#8034. The contacts copy's own
frontmatter says `subDomain: replication`, which is the data-sync draft's whole
domain, and the data-sync copy is longer (98 vs 70 lines) and fully anchored. My
recommendation is to delete `contacts/8034` in a corpus-repair change; that is a
landed-corpus decision, not this PR's, so nothing here does it.

One finding does transfer and is worth carrying over: `data-sync/9593` names
`api/src/services/authorization.js` three times, and those replication services
moved under `api/src/services/replication/` in #10823 (2026-05-11). The same
drift I disclosed on the contacts copy applies there, undisclosed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hareet added a commit that referenced this pull request Aug 26, 2026
…y-pipeline for review (#122)

* chore(memory): promote strong-fit forms-and-reports drafts for review

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

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 (forms-and-reports)

Per sugat009's review on #122: collapse all 6 flagged clusters plus the
6 old-vs-new collisions the relink surfaced (12 total) to one memory
per issue with source_prs[] — backport sets 8745/9429/9604, layered
features 10040/10041, and folds into the curated issue-keyed memories
for 10133, 8225, 8306->8308, 8806, 9227, 9301. The 10922/11116
attachment-routing near-dup collapses to the child issue #10904 with
the epic #10700 noted in prose. 16 files removed.

Cross-domain: the #9835 pair (10022, 10246) moves to the contacts
canonical that owns the issue; the misdomained smsparser draft (10730)
drops in favor of messaging's #10729 memory; the curated 10443/10509
memories absorb the infra (#10445) and contacts (#10570) branch PRs.

8740 keeps issue #7462 (title names it; the closed Enketo-uplift issue
matches the work) rather than dropping as no-issue. Category fixes per
issue labels: 9414 and 9840 -> improvement. Forced domain fits
re-annotated weak (9512, 9513, 11023, 9641); 9592 stays strong.
Reviewer/process narrative and classifier phrasing scrubbed.

All 47 mappings verified against the live cht-core API (0 mismatches);
validate-schema 95/95; no duplicate issueNumbers.

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

* fix(#135): record tasks seeder PR on the #9974 memory (forms-and-reports)

Companion to the tasks (#123) seeder's cross-domain dedup: this corpus
canonically owns issue #9974 (open contact edit form from task), so the
duplicate draft dropped there is recorded here — PR #9975 added to
source_prs with a one-line account of the shipped mechanism.

validate-schema 95/95; no duplicate issueNumbers.

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

* fix(#122): the four review items, each re-derived before editing

9227 — the reviewer is right and the case is stronger than the comment.
`cht:luhn-check` is absent from every ref, not just master:

  git log --all -S'cht:luhn-check' --oneline          # empty
  git log --all -S'cht:validate-luhn' --format='%h'   # 3c2b140a3, one commit
  git tag --contains 3c2b140a3 | sort -V | head -1    # 4.10.0

Renamed throughout. Reading the source turned up two the review did not
cover: Testing claimed an empty-string edge case that none of the sixteen
tests under describe('#validate-luhn()') exercises, and the usage example
dropped the real optional expLength argument. Both corrected, and the draft
now notes that the same commit registered cht:strip-whitespace, which is why
spaced input passes.

9592 — kept at domainFit: strong, against the review. The premise is that it
is the same class as 9512/9513. 9512 is a canDeactivate guard across eight
*.routes.ts; 9513 is a localStorage date check in training-cards.service.ts;
neither touches form-engine code. 9592 adds
training-cards-form.component.ts, which builds an EnketoFormContext, calls
XmlFormsService.get() and FormService, and implements renderForm() and
saveForm(), plus three real XForm fixtures. Grading it weak would make the
corpus less consistent, not more. The rationale now cites the component so a
reader can check the call instead of trusting the grade.

11165 — stripped the scaffolding ("so no pitfall redirects apply"). Swept the
corpus for the same shape; it was the only one. The "least-bad home"
sentences in 9512, 9641 and 11023 are ordinary rationale prose and stay.

9512 — its Domain Rationale said the guard is wired across "seven" feature
modules, which is the phrasing from the round-1 review comment, whose own
parenthetical lists six. It is eight, and the draft's own Solution enumerates
all eight, so the draft contradicted itself:

  git diff-tree --no-commit-id --name-status -r -M 49dcd919a | grep -c routes.ts   # 8

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

* fix(#122): the claims the grounding and coherence gates flagged

Every one re-derived from cht-core before editing, because three of the
gate's own findings turned out to be wrong (see the next commit's note and
the tooling branch).

Fabricated symbols, with the real name established from the tree:
  8806  ValidationResult / validation_result.js — the PR DELETES that file
        (D in its own diff) and ADDS validation_utils.js; ValidationResult
        appears nowhere. isSubmittedInWindow is fictional too; the real
        validators are exists, unique, uniqueWithin, validPhone, uniquePhone,
        isISOWeek, isAfter, isBefore. Its Solution also still described the
        architecture the PR removed, contradicting Design Choices.
  10071 createReport / 10180 updateReport — the API is Report.v1.create and
        Report.v1.update inside a namespace; createReport survives only as a
        test stub name. 10180 also listed test/input.spec.ts, added on no ref.
  8759  contact_by_parent — the view is contacts_by_parent. The draft copied
        the typo from issue #8074, which is worth knowing about issue bodies.
  9301  user.summary — the binding is userSummary, and it gates form
        visibility, not data entry.
  9340  "appearance: number tel" — it is "numbers tel"; numbers is what makes
        the field render as input[type=tel].
  10784 quoted new CustomEvent('before-save', …) as the fix. That string is in
        no commit; the PR imports enketo-core's factory and calls
        events.BeforeSave().
  10922 findBinaryNodeByFilename — real name findFileNodeByFilename, and it
        matches [type=file]: Enketo's Nodeset.setVal rewrites file-widget
        nodes from binary to file on upload.

10922 also asserted, in present tense with stale: false, a mechanism on no
branch reachable from master:

  for s in 0df57c664 cc34e08664 e88c88361; do
    git merge-base --is-ancestor $s origin/master && echo YES || echo NO; done   # NO NO NO

It now opens with a banner naming the three feature branches and is
stale: true. Its Related Issues also claimed #10904 was closed by this PR;
the issue is still open, which is consistent.

added-vs-modified: 10064 called three test files added — all three are M and
the PR's only added file is shared-libs/lineage/src/index.d.ts. 8759, 8806
and 8826 each described a modified test file as added.

9608's mechanism was inverted: the pre-fix validators were too strict
(parseInt(value,10) === value against SMS-parsed strings, so integer always
returned false) and the fix RELAXES five predicates to == with an explicit
eslint-disable. Its backport sentence is left byte-identical — it is true.

9974 described the issue's proposal rather than the shipped code: modifyContent
is a partner-authored task-config callback, only content.edit_id is set, and
the routing is in tasks-content.component.ts::performAction.

10071/10180 are epic children of #10083 and now carry a Provenance section.
Their source_sha values are restored, not "corrected": both are exactly what
GitHub reports as the PR's merge_commit_sha, and are missing from a clone only
because the epic squashed them away.

8740 was keyed to #7462 ("Make code for Enketo forms reusable outside
cht-core"), a different ticket; the epic's ticket is #7599. Re-keyed and
renamed so the filename token stops contradicting the frontmatter.

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

* fix(#122): defects in the siblings the review list never reached

Sweep of all 41 drafts against cht-core and the linked issue bodies. Thirty-
five carried at least one defect; the review list covered four of them.

Inverted mechanism — the draft describes the wrong fix:
  10304 said the deselect handler was corrected. deselectAllReports() is not
        in the diff. areAllReportsSelected() compared the selection against
        reportsList — the RENDERED page — so with more selected than rendered
        the checkbox drew unchecked and the template's ternary re-ran
        select-all. That is why #9739's repro says "at least 50 reports".
        Five sections were wrong.
  10949 said the validator strips XML comments so comments mentioning
        <!DOCTYPE are not flagged, and listed "no false positives" as tested.
        There is no comment stripping, and the PR's own test asserts such
        forms are REJECTED. Four sections were wrong; the check is
        deliberately comment-blind and now says so.
  9641  described a per-form try/catch that skips the broken form. There is
        no loop: updateAll() moved into its own try/catch that logs instead
        of process.exit(1), and still aborts at the first bad form. The draft
        now records that limitation, which is the useful part.
  8656  inverted the symptom entirely — this is "xpath extensions tests fail
        in my timezone", fixed by pinning Date.prototype.getTimezoneOffset in
        a beforeEach, not a runtime inconsistency between extensions. Retitled
        and renamed, and category chore -> improvement (chore is not in the
        schema enum).
  9414  said the listener "never fired"; the issue reports a stale-by-one read
        in enketo-core's CI only. The macro-task fix is in the karma spec, not
        the e2e spec.
  11165 described a guard that blocks conversion. The widget cannot gate the
        library: it lets the conversion happen, clears the output, and
        re-asserts on the next tick to beat the library's own blur handler.

Attribution to files the PR never touched:
  10133 put the _all_docs-with-attachments call in generate-xform.js; it is
        forms.js.
  10509 said it reused the enketo service's extraction logic. That service is
        not in the PR and the originals are private, so the logic was
        re-implemented — which is why #11256 later merged the paths.
  9840  credited enketo.service.ts / form.service.ts with extension-lib
        injection; it goes through the cht-form stub datasource. Those files
        changed because EnketoFormContext became an interface.
  9755  claimed a freetext-index fallback "for search strings containing
        whitespace", in six sections. No such fallback exists; the keyed-vs-
        range split lives in cht-datasource and keys on a colon.
  8336  pointed at webapp/src/js/enketo/widgets.js, which #10269 does not
        touch and which has nothing to do with xforms-value-changed — that is
        enketo.service.ts:327 and the transformer XSL. Also given a
        source_prs entry: it had no PR reference in frontmatter at all, which
        is why its anchor would not resolve, even though its prose names
        #10269 and its "78 files" claim is exactly right.
  10814 called extensionLib a method on XmlFormsContextUtilsService; the PR
        removed every public method in favour of an async get() factory.

stale-as-written: the drift epic here is cccce201e refactor(#10700): re-write
Enketo form save workflow (#11256), which deleted contact-save.service.ts and
enketo-translation.service.ts. 10509, 10784 and 10922 are time-scoped against
it rather than silently corrected to master's shape. Nothing in this batch
records that rewrite, because the #10700-keyed draft was the one dropped in
the round-1 dedup.

10937 category improvement -> feature: issue #9339 is Type: Feature and the
commit is feat(#9339).

10290, 10756 and 8949 were checked and left alone.

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

* fix(#122): the residue a second gate pass and the online audit turned up

Repeated passes over the corrected drafts, which is the point of the count.

verify-drafts --online, 3 blocking + 1 weak cross-reference, all introduced by
this round's own edits:
  10071/10180  cited #10083 in Related Issues glossed as "the epic PR whose
               squash carries this work" — true of the PR, but it is a PR cited
               as an issue and the gloss shares nothing with its title. Now
               labelled PR #10083 and glossed with its real subject.
  10071        cited #10038 as "the broader report-creation feature". #10038 is
               "To have API that can create places" — the PLACE half of the same
               datasource work, not the report half. Re-glossed.
  8740         glossed #7674 as "Enable excludeNonRelevant in the Enketo
               config". That is the fix; the issue is "Answers to non-relevant
               questions in forms are not immediately cleared with new Enekto".

ground-claims, second pass. The one that matters:
  10509  the previous commit justified the duplicated extraction by pointing at
         enketo.service.ts's private processFormAttachments /
         buildBinaryAttachmentData. Those did not exist at this PR's anchor:

           git log --all -S'processFormAttachments' --format='%h %ci'
           #   ec882d703 2026-07-17   ← five months AFTER d09d656cb8

         At the anchor the logic was inline in xmlToDocs with no callable
         helper, which is the real reason the contact path re-implemented it.
         An anachronism introduced while fixing something else — exactly the
         failure this exercise is about, committed by the person fixing it.

  8806   put pupil's validator map in pupil.js; it is validator_functions.js,
         looked up by validator.js.
  8759   "added/updated" for a file that is only modified — the hedge read as
         "added" to the probe, and to a reader.
  10756  dropped "Full Enketo regression suite … passed 103/103" — a run-log
         artefact naming a path (tests/karma/js/enketo) that does not exist.

Un-greppable literals rewritten so they can be checked rather than trusted:
  10443  "training:admin:1234" was an instantiation; the code has the template
         literal training:${USERNAME}:1234.
  9513   "training-cards-last-viewed-date-<username>" likewise; the constant is
         STORAGE_KEY_LAST_VIEWED_DATE, suffixed by getLocalStorageKey().
  8336   config/*/forms/ and tests/**/forms/ are globs, not paths; replaced
         with the real trees.
  9340   instance::cht:unique_tel stays — it is an XLSForm column header and
         real — but the draft now also names the greppable artefacts it becomes
         (cht:unique_tel in the instance, data-cht-unique_tel on the question)
         and says why the header itself cannot be found in the tree.
  10071/10180  unbackticked merge_commit_sha, a GitHub API field the probe was
         reading as a cht-core symbol.

lastUpdated on 10290, 10756 and 8949 set to their real last-edit date rather
than today: their content was not changed this round, and the stale-timestamp
warning was inherited from an earlier one.

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

* fix(#122): six contradictions the second coherence pass sampled

Each side settled against cht-core rather than reasoned about. One of these
(9340) was found by the FIRST coherence pass and I failed to hand that report
to the sweep, so it survived a whole round — worth recording as a process miss
rather than quietly fixing.

  9340  Domain Rationale said "the contact duplicate-check is a secondary
        capability of the widget, not the subject of the change", while the
        summary says the PR makes the dup-check opt-in. The dup-check IS the
        subject. Rewritten.

  10842 summary and Problem both said arrays were "always inserted into repeat
        groups". Pre-fix, an array aimed at an ordinary field was refused:

          git show 018037e56^:webapp/src/js/enketo/widgets/android-app-launcher.js
          #   if (Array.isArray(value)) { console.debug(… "value is an array"); return; }

        Nothing was written at all. Repeat insertion was only ever available
        through the android-app-value-list appearance.

  9301  Code Patterns said the summary is "loaded once per form session";
        Design Choices said the cache avoids recomputing it every time a form
        opens. The cache is a CacheService entry invalidated by
        ContactChangeFilterService.isRelevantChange — it outlives a session
        entirely. Code Patterns was the stale side.

  10133 Problem called the update-path read "the same" read as the startup one
        while Root Cause calls them two separate reads in two files. Both are
        true of different things; disambiguated.

  8308  Design Choices "added draw and file-upload integration tests" vs
        Testing "E2E test for photo upload forms (updated)". Both correct —
        different files (the integration specs are A, the e2e spec is M) — so
        this is a checker false positive. Rewritten anyway to name the files:
        if a checker misreads a sentence, a reader will too.

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

* fix(#122): two contradictions my own previous two commits introduced

Both are the same failure mode: correcting one section and leaving its sibling
asserting what was just disproved. Worth naming rather than folding into an
earlier commit, because the count of rounds is the honest signal here.

  10071  I re-glossed #10038 in Related Issues as the place-creation sibling
         (it is "To have API that can create places") but left Design Choices
         calling it "the broader create-report feature this work feeds into".
         The report half is #10040, which is this draft's own issue.

  10509  I rewrote Code Patterns to say the extraction was inline in xmlToDocs
         with no callable helper — and left Root Cause still saying it "lived
         in private methods there". The private methods arrived with #11256,
         five months later.

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

* fix(#122): what the fourth ground pass found, three of them mine

  8336  My own previous commit replaced two glob patterns with a list of
        config trees — and invented two of them. #10269 touches
        config/default/forms (22 files), config/demo/forms (21) and
        config/covid-19/forms (10). There is no config/default-bs or
        config/standard in its diff:

          git diff-tree --no-commit-id --name-only -r 5fcfbcbafb \
            | cut -d/ -f1-3 | sort | uniq -c | sort -rn

        Replacing an ungreppable glob with a wrong path list is a worse
        failure than the glob was. Now states counts and names real files, so
        the claim is checkable instead of merely well-formed.

  9227  My rewritten Testing section quoted describe('#validate-luhn()'), a
        literal that `git grep -w` can never match. Points at the greppable
        cht:validate-luhn instead — the same blind spot the docs already
        record for placeholder literals, walked into while fixing something
        else.

  9340  My note about the XLSForm column header attributed cht:unique_tel to
        phone-widget.js. It is not there: cht:unique_tel appears in the
        generated XForm instance, and what the widget reads is the
        data-cht-unique_tel attribute (phone-widget.js:61). Both now named in
        the right place, which is the whole point of the sentence.

  8759  "The original `with-same-parent` naming was ambiguous" — that name is
        in no commit on any ref; it existed only in review discussion.
        Recording a dead name as though it shipped misleads a reader, so the
        rationale now argues from what the filter does rather than from a name
        that never existed.

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

* fix(#122): the fifth pass — two stragglers and a third round on 10509

  10071  Design Choices justified the local work as "parity with the remote
         context", while the draft's own Problem says the remote side had no
         create implementation either. Both halves were added by the same
         epic, so this is one side of a paired addition, not a catch-up.

  10509  Third round on the same sentence. I corrected Root Cause, then Code
         Patterns, and Design Choices still said the enketo service's version
         was "private to that service" — the framing that was wrong in the
         first place, since there was no method there at all until #11256.
         Recording the round count rather than smoothing it over: this is the
         draft that shows why one clean pass proves nothing.

  8740   "enketo-core's `relevant.js`" reads as a repo path and is reported
         ungrounded on every pass, because it is a third-party file. Says
         "enketo-core's own relevance module" instead, and points at
         webapp/patches/enketo-core+7.2.5.patch — a real path in this repo —
         for where CHT actually overrides it. The claim is unchanged; it is
         now checkable.

8336's remaining finding needed no draft change: "create" inside the fixture
name ngo-create.xlsx was being read as a create verb. Fixed in the tooling
instead (memory/draft-verification d62a907) with regression tests.

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

* fix(#122): the sixth and seventh passes — including one correct claim we broke

The important one first.

**10071: an earlier commit on this branch replaced true claims with false ones.**
The sweep concluded that `createReport` was fabricated and that
`src/qualifier.ts` "was never part of this work", both checked against the
epic squash — because at that moment the child PR's own merge commit was not
in the clone. It is now, and it says the opposite:

  git diff-tree --no-commit-id --name-status -r -M cab214534 d40e65bae7
  #   M shared-libs/cht-datasource/src/local/report.ts
  #   M shared-libs/cht-datasource/src/qualifier.ts
  #   M shared-libs/cht-datasource/test/local/report.spec.ts
  #   M shared-libs/cht-datasource/test/local/person.spec.ts

  git show d40e65bae7:shared-libs/cht-datasource/src/local/report.ts | sed -n '81p'
  #   export const createReport = ({

`createReport` is real at this PR, taking a `ReportQualifier` and rejecting
`_rev`; the qualifier.ts change is one line, exporting `ReportQualifier` so
the adapter can name it. The #10083 squash then renamed the operation to
`Report.v1.create`, moved it into a `v1` namespace and replaced the qualifier
with `Input.v1.ReportInput` from a new `src/input.ts` — none of which is in
this PR's diff.

So the original draft was right and we corrected it into being wrong. The
draft now records both views and says which is which, the way the 10140 epic
children on #123 do. The flat statement "there is no `createReport` symbol
anywhere in cht-core's production code" is gone; it was false.

This is the exact laundering this exercise exists to prevent, and it happened
here because an anchor moved between passes: the clone acquired the child
commit mid-run, so passes before and after disagree about which tree to judge.
Worth knowing that an unresolvable anchor is not a stable property of a clone.

Also:
  8336   cited ngo-create.xlsx as a regenerated fixture; it is `A`, the one
         file the PR adds. Cites two genuinely regenerated fixtures instead.
  8740   a second `relevant.js` in Design Choices, sibling of the one already
         reworded — the same third-party path in the same draft, missed first
         time because only one occurrence was quoted.
  10756  said the widget parses `cht:unique_tel`. It parses the rendered
         `data-cht-unique_tel`; `cht:unique_tel` is what pyxform emits into the
         XForm instance.
  9340   "behavior is selected from appearance … rather than the field type"
         was too absolute. `_init` reads
         `$wrapper.attr('data-cht-unique_tel') === 'true' || deprecated.isDeprecated($wrapper)`,
         so the legacy shape still turns dup-checking on through the second
         branch — which is why the draft can say legacy behaviour is preserved
         without contradicting itself.

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

* fix(#122): the eighth round — seven findings across six passes

Three ground passes and three coherence passes on frozen bytes turned up
seven things, so this is not yet the clean run. Three of the seven are
collisions between my own earlier fixes.

Ground:
  10071  `entities` still listed shared-libs/cht-datasource/src/input.ts.
         That file is the epic's, not this PR's — the PR changed four files
         and input.ts is not among them. Dropped; the Related Files section
         already explains that input.ts arrived with the squash.
  8740   a THIRD `relevant.js` in the same draft, this one in Solution. Two
         earlier rounds each fixed one occurrence and left the others,
         because only the quoted sentence was looked at.
  8746   `form.properties.json` is a filename pattern, not a path — nothing is
         called that. Names the real fixture,
         tests/e2e/default/tasks/forms/home-visit.properties.json.
  9641   `form:broken` never appears in the source; the test builds
         ``form:${formName}`` with formName = 'broken'. Quotes the template.

Coherence — all three of these are mine:
  9340   I added a Code Patterns note that the legacy `type: tel` shape keeps
         always-on dup-checking through `deprecated.isDeprecated`, and left
         Domain Rationale saying the dup-check is simply "opt-in after it".
         Flagged in all three coherence passes. Domain Rationale now states
         both paths.
  9227   I added "the same commit also registered `cht:strip-whitespace`" to
         Solution, and left Design Choices justifying the file placement
         "since it is a single function".
  10922  Design Choices says routing is by XML position "rather than flat
         filename-to-doc matching", while the known-limitation sentence
         describes a filename lookup misrouting files. Both are true of
         different steps: the ancestor walk picks the owner doc, but
         findFileNodeByFilename picks the node the walk starts from. Said so.

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

* fix(#122): two contradictions the twelfth coherence pass sampled

Ground has now been clean three passes running; coherence had one pass in
three that was not, which is the whole argument for the pass count.

  10071  Problem said "person and place already had a full create path".
         Person did, on the epic branch — at this PR's parent cab214534 there
         is Person.v1.createPerson, api/src/controllers/person.js's
         createPerson, and postResource('api/v1/person') in remote/person.ts.
         Place had none, which is why #10038 is still open as the sibling
         half. Half the sentence was right, which is what made it survive
         eleven passes.

  8826   Root Cause's "no mechanism to carry a per-field duration through form
         generation" sat beside its own note that the old note-based timer
         read a duration off the note's value, and beside Design Choices
         calling the new column a mirror of that prior capability. Internally
         consistent if read carefully, and read as a contradiction by the
         checker — the same call as 8308 last round: if a checker misreads a
         sentence, a person will too. Scoped the "no mechanism" to form
         generation and said plainly that durations were already configurable
         the one way that needed no generation support.

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

* fix(#122): round ten, and the timestamp bump, in one commit

Ground was clean three passes running (g11-g13, 0 ungrounded each); the next
three found four more. That is the sampling, not a regression — same drafts,
different claims drawn each time.

  10071  my own sentence from the last commit quoted
         `postResource('api/v1/person')`. The real call takes the context
         first — postResource(remoteContext, 'api/v1/person') — so the literal
         matches nothing. Names the symbol and the route instead of inventing
         a call form. Third time a fix of mine has introduced an ungreppable
         literal.
  10071  Design Choices said #10099 "reused the existing person/place create
         architecture" while Problem now says place had none. Person's was
         standing on the epic branch; place's was not. Flagged by two of three
         coherence passes.
  8225   three enketo-core module paths (src/js/relevant.js, src/js/form.js)
         read as repo paths. Says "enketo-core's relevance module" and "form
         module", keeping webapp/patches/enketo-core+7.2.5.patch — a real path
         here — as the anchor a reader can actually open.
  10917  "renaming the widget from HiddenFieldList/hidden-field-list.js to
         HiddenGroup/hidden-group.js". hidden-field-list.js is in no commit on
         any ref: the PR adds hidden-group.js outright, and the narrower name
         existed only in the review iterations. Recording a name that never
         landed sends a reader looking for a file that was never there.
  10133  summary said "a single _all_docs call" while Root Cause says two
         separate reads in two files. Names both paths.

Two findings deliberately NOT acted on, because the drafts are right:

  10922  "PR #11116 … updated downstream contact rendering —
         contact-save.service.ts, format-data-record.service.ts,
         contact-photo.component.ts, contacts-content.component.ts". The probe
         judged that at #10922's anchor, where none of those files exist. But
         the sentence credits #11116 explicitly, and e88c88361 touches all
         four (contact-photo.component.ts among the files it adds). The
         enumerate-claims layer already declines to infer a status when
         another PR is credited; the path probe does not yet apply the same
         rule.
  8806   a contradiction reported with the literal title "placeholder" and two
         unrelated quotes — the checker emitted a finding with no rationale.

lastUpdated set to 2026-08-10 across all 41, folded in here rather than left
for a follow-up: a metadata-only commit would reset the convergence streak for
no content reason, which is how the last round lost its clean ground passes.

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

* fix(#122): the three findings that outlived the last commit

Passes g17-g19 and c17-c19 all ran after 1c7b4d2, so their findings were about
committed bytes and nobody acted on them. g19 and c19 came back clean and the run
stopped there, but c17, c18 and g18 had each surfaced something on that same
content first. Clean-at-the-end is not the same as converged.

10922  the epic-work sentence named `contact-photo.component.ts` as a bare
       filename. PR #11116 really does add it, at
       webapp/src/ts/components/contact-photo/contact-photo.component.ts, which
       the Related Files list already gives in full — but the bare name resolves
       nowhere at this draft's anchor, so a reader greps and finds nothing. The
       sentence now describes the component and leaves the path to the list that
       already scopes it to #11116.

10922  Design Choices asserted the ancestor walk "ensures attachments land on
       the structurally-correct owner" and then, in the same paragraph, gave the
       limitation that defeats exactly that: the walk starts from a node found by
       filename, so two same-named files start from the same node and land on
       the same sub-doc. Scoped the guarantee to the caveat it already states.

8656   Root Cause claimed the `asMoment()` fallthrough "re-parsed the raw input
       instead of returning the rMoment it had already built, so the two paths
       could disagree", while Problem said no deployed behaviour was wrong. The
       source settles it for Problem: the branch is `return moment(r)` with
       `const rMoment = moment(r)` in scope and `r` never reassigned between
       them. Same parse, same string, twice — redundant, not divergent. Reworded
       Root Cause and the Solution's description of the tidy-up.

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

* fix(#122): record the deferred data-access decision in the drafts it concerns

The review asked for `10064`/`10071`/`10180` to move to a `data-access` primary
domain with `secondaryDomains: [forms-and-reports]`. The decision to defer that
— taken on the reviewer's own recommendation to ship the enum value and the new
field as one coordinated taxonomy change rather than through a content PR — lived
only in the review thread, so a reader of the corpus saw three cht-datasource
extenders keyed `forms-and-reports` with nothing explaining why.

Each of the three now records it in Domain Rationale: the files are entirely
shared-libs/cht-datasource, that makes them library extension rather than
consumption, the re-key was requested, and it is deliberately not made here.

`10071` had no Domain Rationale section at all — the one draft the reviewer's
domain comment was actually filed against, and the section where the answer
belongs. It has one now. It is the only machine-distilled draft missing it; the
other ten without the section are the hand-authored files, which is the
two-schemas item already flagged as not blocking.

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

* fix(#122): six more, and one of them was mine from the commit before

Three ground and three coherence passes over the frozen bytes of 789f817. Four
of the six are the corpus; two are probe artifacts fixed by making the draft
more precise rather than by arguing with the gate.

10064  MY error, flagged by all three coherence passes. The Domain Rationale I
       added in 789f817 said the files are "entirely shared-libs/cht-datasource".
       The diff also carries api/src/controllers/report.js,
       shared-libs/lineage/src/index.d.ts, two webapp files and integration
       tests — which its own Solution section already spelled out. Reworded, and
       it now notes that the review's "touches only cht-datasource" premise is
       weaker for this draft than for 10071/10180.

9608   Root Cause said the integer predicate "returned false for every input"
       while Design Choices said loosening `===` to `==` "preserves behaviour for
       callers already passing numbers". Both cannot hold. The source settles it
       against Root Cause: `integer: (allValues, value) => parseInt(value, 10)
       === value`, so a numeric 5 was already true and only SMS-parsed strings
       failed. Scoped the claim and stated why numeric callers are unaffected.

10842  The summary conditioned the join on "the target field is not a repeat"
       while the Solution says no repeat detection exists — the join fires on any
       all-primitive array, and repeats are unaffected only because their helpers
       pass one element at a time. Summary now describes the trigger the code
       actually uses.

9641   `--skip-validate` is a cht-conf flag, probed against cht-core where it can
       never appear. It is real (cht-conf src/cli/usage.js:71); the draft now says
       whose flag it is.

10071  "a postResource call ... in `src/remote/person.ts`" is true at the parent
       commit cab214534:14, but the partial path resolves nowhere. Full path.

10917  "The widget is registered in webapp/src/js/enketo/widgets.js" is true —
       widgets.js:34 is `require( './widgets/hidden-group' )`. The probe looked
       for the `HiddenGroup` symbol, which is not how registration works here.
       The draft now says so, which is the more useful sentence anyway.

Not fixed: 10784 quotes `import events from 'enketo-core/src/js/event'`, a real
line at enketo.service.ts:6. It is a package specifier, not a repo path, and the
probe cannot tell the difference. Left as written and disclosed.

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

* fix(#122): two summaries that under-described their own Root Cause

Both from the second convergence set; ground came back clean three passes running
(g23-g25, 0 ungrounded each) and `verify-drafts --online` cleared all 41 with 0
unverified, so these two are what is left.

10784  The summary blamed the `end`-timestamp bug on the jQuery trigger not
       reaching a native listener, while Root Cause names two defects and the
       event name is the first of them. The diff bears that out —
       `$('form.or').trigger('beforesave')` became
       `form.view.html.dispatchEvent(events.BeforeSave())`, changing both the
       dispatch mechanism and the event identity. Summary now carries both.

10917  Testing said "Added Karma unit tests (…hidden-group.spec.ts)" and then
       "Neither the spec nor the fixtures were newly created here". Both are true
       of different files and the second reads as denying the first.
       `git show --name-status 23225a57d7`: the Karma spec is `A`, the e2e spec
       and both db-object fixtures are `M`. Says which is which now.

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

* fix(#122): the place-create claim I left behind, and an empty related_issues

10071  Code Patterns said the report create chain mirrored "the existing
       person.ts and place.ts implementations" while Problem says place had no
       create path yet and that #10099 added it. Person's was standing at the
       parent commit cab214534; place's was not. Corrected to name person only
       and point at Problem.

       This one is mine twice over: the same contradiction was flagged by the
       first convergence set, I fixed the postResource path on this draft in the
       same round, and left this line untouched. A finding read is not a finding
       fixed.

10922  Frontmatter carried `related_issues: []` while the Related Issues section
       lists three. Populated with the two that are genuinely related — the
       #10700 epic and the #10903 sibling — leaving out #10904, which is the
       draft's own issue.

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

* fix(#122): read versus written, and a switch that does not exist

Three from the fourth convergence set. Ground is clean six passes running
(g23-g28) and the online audit cleared all 41; coherence is the only channel
still finding anything.

10133  Two, both in the same draft.

       The summary blamed the timeouts on both reads. Root Cause is more precise
       and correct: the `_all_docs` batch read is the one that hangs
       (apache/couchdb#2210), while the per-doc `get` on the update path was
       expensive, not hanging. Summary now says which is which.

       The Solution said the attachments "read and saved" are the XForm XML
       "plus `model.xml` and `form.html`", which contradicts Design Choices
       skipping everything non-XML. Reading the code settles it: `getFormDocs`
       fetches only the attachment named by `getXFormAttachmentName`, and
       `model.xml` / `form.html` are outputs generate-xform.js writes back
       (:243, :247). Read and written are now separate claims.

9340   Design Choices said validation and the duplicate check "can be enabled
       independently". The PR's own fixture shows otherwise: the new field is
       `type: string` with `appearance="numbers tel"`, which always validates
       format, and only the uniqueness lookup is opt-in via
       `cht:unique_tel="true"`. There is no switch for the format half.

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

* fix(#122): 8225 justified a CHT-specific patch as a general one

Design Choices said the fix belonged in enketo-core "since the behavior was
incorrect regardless of CHT-specific logic", while Solution says the patch
special-cases the `inputs` group. Both cannot be the reason.

The patch settles it. webapp/patches/enketo-core+7.2.5.patch carries the comment
`// CHT-CORE PATCH` / `/inputs is ALWAYS relevant #4875` and matches
`/^\/[^/]+\/inputs$/` — a hard-coded CHT form convention, not upstream-correct
behaviour. The real reason to patch enketo-core is that relevance is evaluated
there, so it is the only layer where the branch can be kept enabled; and being
CHT-specific is exactly why it sits in webapp/patches instead of going upstream.

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

* fix(#122): round-3 review — seven items, two of them mine

Every item verified with the reviewer's protocol before editing, which is the
protocol I had not been using: quote at head, check at the draft's anchor, check
on master, AND walk the commits that touched the region in between. The last step
is what my gate never did, and it is how two of these got in.

10133  MINE, from 04c3958. "Only one attachment is read" and "`model.xml` and
       `form.html` are not read at all" are both false. PR #10248's own diff ADDS
       three named reads in `updateAttachments` (generate-xform.js) —
       getXFormAttachmentName(doc), 'form.html', 'model.xml' — in one Promise.all
       feeding addGeneratedAttachments. I had checked getFormDocs in forms.js,
       found its single named read, and generalised to the whole PR without
       opening the other file the same PR changed. Reframed as what it is: named
       reads replacing bulk reads. Code Patterns and Design Choices carried the
       same wrong claim and are corrected with it.

10071  MINE, from a9f5dcf. Fixing one contradiction I asserted another: place.ts's
       create path was NOT "added alongside this work by the same #10099". It was
       already standing — #10065 (local, 169a02355, 2025-06-25) and #10089 (API,
       98a687a80, 2025-06-26), with #10094 moving the surface to `Input`, all
       before #10099 landed 2025-07-03. Line 59's "Place had none yet" is true at
       this draft's anchor cab214534, so only its parenthetical needed the fix.

8656   Sign error. `getTimezoneOffset = () => -240` emulates UTC+4, not UTC-4 —
       the distilled commit's own spec proves the convention (`-60` asserts
       '+01:00'). Scope was over-strong too: the suite was timezone-dependent,
       failing in some zones (NZ +12, per #8556) and passing in others including
       UTC in CI, not "passing only at UTC-4". Also narrowed "every
       to-bikram-sambat case" to the conversion cases; the 11 invalid-input cases
       assert an empty string and are timezone-independent.

9755   From b82840f. The ':' separator does not route between Nouveau and the
       offline path. `useNouveauIndexes` picks Nouveau when the
       _design/medic-offline-freetext ddoc is absent; ':' (isKeyedFreetextQualifier)
       only selects keyed vs prefix-range within a path. At this anchor Nouveau
       was not in the path at all — it arrived later in f1bdfc07c (PR #10201).

10922  Both PRs ARE merged, into epic branches: #10922 into
       10700-photo-capture-in-sub-contacts-and-reports on 2026-05-19 (squash
       cc34e08664, which is that branch's head), #11116 into
       5.1.2-FR-attachments-for-subcontacts on 2026-05-29 (squash e88c88361). The
       banner said unmerged, called 0df57c664 a branch head when it is an interior
       commit of 10700-photo-capture, and claimed the work lives on all three
       branches when neither sha is in the 5.1.2 one. Frontmatter summary and the
       "unmerged epic branch work" concept follow.

8740   The squash's only test change is +2/-2 adding 'deprecatedID' to two
       excludingEvery() lists — edit-metadata fallout of the v7 uplift, not
       relevance/widget coverage. #8740's own Escape-workaround removals cancelled
       out inside the epic branch and are absent from the squash.

8759   Removed. It duplicated the identity of a landed contacts draft: both carry
       issueNumber 8074, and agent-memory/domains/contacts/issues/8074-... has
       owned that key on main since #79. Same change, identical 7-file Related
       Files set. Its unique content — the concrete spec paths in Testing and the
       descendant-of-current-contact naming rationale — is recorded in the PR reply
       for whoever folds it into the contacts canonical; it is not re-keyed to
       8759, which is a PR, not an issue.

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

* fix(#122): name the attribute that greps, not only the column that does not

8826 described the passthrough only as `instance::cht:duration`. That is the
XLSForm column header, which lives inside the .xlsx workbook and appears nowhere
in the tree — a ground pass reported it as a non-existent symbol, and a reader
grepping for it finds nothing. The rendered XForm attribute is `cht:duration`,
present in five files on master. Both forms are now named, with which is which.

Same shape as the `--skip-validate` fix earlier in this branch: the claim was
true, just stated in the one notation that cannot be checked or found.

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

* fix(#122): pin the enketo-core event name to a version a reader can check

Self-audit of the sentences this branch added, using the protocol the round-3
review used rather than the one I had been using. Four assertions in my own new
prose had not been independently settled; three checked out against source
(10071 and 10180 really are 4/4 and 2/2 files under shared-libs/cht-datasource;
8740's squash test diff really is +2/-2 adding 'deprecatedID', with the Escape
workaround absent at 314e79061a^; issue #8556 really does report NZ +12).

The fourth could not be settled from this checkout at all: "enketo-core's
event.js defines the event as `before-save`" was carried on the reviewer's word
because enketo-core is not installed here. It is true — webapp/package.json pins
^7.2.5, and 7.2.5's event.js has `BeforeSave()` return
`new CustomEvent('before-save', { bubbles: true })` — so the sentence now names
the version, which turns a claim a reader has to trust into one they can look up.

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

* fix(#122): say what the web component actually wires, and unknot a sentence

9301  "Replicate the same enketo wiring in cht-form's app.component.ts" invited a
      ground pass to bind `user-contact-summary` to that file, where it does not
      appear. The wiring is real but spelled differently there — a `contactSummary`
      input and an `instance[id="contact-summary"]` lookup — while the
      `user-contact-summary` instance id lives webapp-side in form.service.ts and
      xml-forms.service.ts. Naming both halves is more useful than "the same
      wiring" and stops the mis-binding.

10784 Punctuation only: the version evidence added in ec3adbf turned a
      two-defect sentence into nested em-dashes. Split.

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

* fix(#122): the selector still needs two classes, and Nouveau is master's story

10917  Solution and Design Choices said the matcher was broadened so "any
       top-level group with the `hidden` appearance" is skipped, while Testing
       said the spec asserts a match only when both markers are present. Testing
       was right: the shipped selector is `.or-group-data.or-appearance-hidden`
       (hidden-group.js:16 at the anchor), and the Karma spec asserts a group
       carrying only one of the two classes does not match. What review dropped
       was the `field-list` requirement, not the second class. Corrected in all
       three sections, including the Solution opening that had the same
       imprecision and would otherwise have surfaced a pass later.

9755   My own sentence from 57e7c3a read as though this PR routed through
       Nouveau: "It uses Nouveau when running server side ..." followed
       immediately by "At this draft's anchor Nouveau was not in this path at
       all". Both true — the first describes master, the second the anchor — but
       nothing said so until the second half. Scoped the first to master.

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

* fix(#122): three the confirm set found, all "true but unfindable"

None of these was a wrong claim; each named something in a form no probe and no
reader can resolve, which is the same defect from a usability standpoint.

8806   Root Cause named the pre-fix container as `validation.extra_validations`.
       The map is real — validation.js:157 at this PR's parent — but the source
       spells it `extra_validations`, and this PR removes it, so the dotted form
       greps to nothing at either commit. Names the real key, the file and line,
       and says it is the pre-fix state.

10922  Design Choices credited #11116 with the repeat/cleared/orphaned handling
       while Testing placed the same fixtures with #10922's enketo.service work.
       Both diffs contain all four fixture files; #10922 is where they land with
       the code they exercise. Names the files and notes #11116 carries an
       equivalent copy on its own branch.

10180  `Input.v1.UpdateReportInput` is real on master (src/input.ts:36) and absent
       at this draft's source_sha, because the epic squash introduced input.ts.
       The Provenance section says so draft-wide; the sentence now says it too,
       which is what stops a pass rediscovering it every few runs.

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

* fix(#122): backfill related_issues from the cross-links already in the prose

The new `related-issues-desync` check fired on ten drafts here: each names issues
under `## Related Issues` that the machine-readable field omits. #135 de-duplicates
on the field, not the prose, so those cross-links were invisible to it.

All eleven referenced numbers were confirmed to be issues rather than PRs via
`gh api repos/medic/cht-core/issues/<n> --jq .pull_request` before being added —
adding a PR number to that field is the identity defect this corpus spent three
review rounds removing.

Five drafts had `related_issues: []`; five hand-authored ones had no key at all
and gained one. Warnings on this PR: 13 -> 3.

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

* fix(#122): stamp every draft this branch touched with today

`stale-timestamp` fires when lastUpdated predates the file's own last commit, and
committing yesterday's stamp today creates exactly that. Bumped across the drafts
this branch has changed rather than only the ones edited in the last commit, so
the field means "last reviewed" consistently instead of tracking whichever round
happened to touch a file.

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

* fix(#122): the web component never gets the user summary

Mine, from 7eb5727. Fixing the mis-binding on that draft I wrote a Code Patterns
line saying `user-contact-summary` is webapp-side only — correct — and left the
Solution still claiming the wiring was "mirrored in the standalone cht-form web
component". Coherence pass 61 put the two side by side.

The Code Patterns half is the right one. `user-contact-summary` is zero-hit in
app.component.ts at the anchor and on master; what this PR gave the web component
is an explicit id on the instance it already had:

  -  this.formContext.contactSummary = value ? { context: value } : undefined;
  +  this.formContext.contactSummary = value ? { id: 'contact-summary', context: value } : undefined;

Both sections now say that, including the "so embedded forms behave identically"
clause, which asserted a parity that does not exist — an embedded form cannot
read the user summary at all.

Fourth defect this branch has had from a fix of my own, and the second on this
one draft. The sibling was found by re-reading the section next to the one I
edited, which is the habit the runbook now prescribes for exactly this reason.

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

* fix(#122): name the file that holds showConfirmExit

Not a wrong claim: the draft said the guard "flips a `showConfirmExit` flag ...
in global state", and it does. But "global state" is spelled three ways in the
same paragraph — actions/global.ts, reducers/global.ts, selectors/index.ts, all
genuinely touched by this PR — so a ground pass bound the symbol to the actions
file, where it does not appear. At the anchor it lives in reducers/global.ts,
alongside the guard provider, the modal component and the selectors spec.

Naming the reducer is both more useful to a reader and the thing that stops this
recurring: it is the same "true but unfindable" shape as the `extra_validations`
and `cht:duration` fixes earlier on this branch.

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

* fix(#122): do not assert a fact about a commit this draft says is unreachable

Mine, from 5f8b6fc. Scoping `Input.v1.UpdateReportInput` I wrote that it is
"absent at this draft's own `source_sha`" — a claim about the tree at
`70b7be0b4`, which the Provenance section three paragraphs earlier says is absent
from a clone because the epic squashed it away. I could not have checked it, and
a reader cannot either. Coherence pass 69 put the two sentences side by side.

Replaced with what is actually checkable without that commit: the type is
`src/input.ts:36` on master, and `input.ts` is not among the two files #10180
itself changed — verifiable from the PR's file list, which the API serves whether
or not the merge commit survives.

  gh api repos/medic/cht-core/pulls/10180/files --jq '.[].filename'
  #   shared-libs/cht-datasource/src/local/report.ts
  #   shared-libs/cht-datasource/test/local/report.spec.ts

Fifth defect on this branch introduced by a fix of mine, and the second where the
fix asserted something unverifiable rather than something false.

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

* fix(#122): the sha we called unreachable answers a one-line probe

Round-4 review proved 70b7be0b4 is checkable after all — the 9835 epic
branch is deleted upstream (ls-remote refs/heads/9835* is empty), but the
commit stays reachable through refs/pull/10083/head. 6e43e88 removed a
true anchor claim on the false premise that it could not be tested; this
restores it with what the anchor actually holds: the update validates the
older ReportInput via validateReportUpdatePayload, and the
Input.v1.UpdateReportInput name only enters input.ts with the epic's
#10522 refactor a89955a9f.

The reviewer's suggested text is amended, not applied verbatim: it cites
origin/9835-… as the reachable ref, which only resolves in a clone with a
stale unpruned copy of the deleted branch.

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

* fix(#122): the NgRx files were modified, not created — say so

The new --added-lines delta gate flagged this on its first live run:
"New global NgRx state (actions/global.ts, ...)" enumerates as a
file-touched added claim, and PR #9512's diff shows M for all three files
(only training-card.guard.provider.ts is A). The state slice is new; the
files are not.

Two rewordings failed the same gate before this one passed: "modified
rather than created by this PR" still carries a create-verb near the
paths, and "New global NgRx state, carried in the existing ..." lets the
adjective "new" reach the path list. The committed sentence puts a clause
break between "New" and the paths and states location with "lives in".

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

* fix(#122): apply the five verified round-4 suggestions verbatim

Each was re-derived against cht-core before acceptance: 9301's selector
lives only in form.service.ts:105 and app.component.ts tags-and-renders
(:82, :217, zero FormService imports); 10917's edit was a substitution
(hidden-field-list.js's two-class selector -> hidden-group.js, spec
asserts single-class non-match at :34-35/:40-41); 8826's third notation
is stamped by getChtAttributeEntries (:128-131); 10133's named reads go
through formsService.getAttachment (:258-260, Promise.all :257-261) with
the raw call only in forms.js:31-33; 10784's ^7.2.5 is a floor pinned in
practice by webapp/patches/enketo-core+7.2.5.patch, event.js:185-186 in
7.2.5. Applied byte-exact from the review API, verified with a
round-trip comparison after writing.

The sixth suggestion (10180) was amended in a389ae0 rather than applied:
its mechanism cites a branch deleted upstream.

Co-authored-by: sugat009 <sugat009@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#122): time-scope two names the current tree no longer has

The dense delta grounding (all three Opus passes, identically) surfaced
two stale-as-written names the sampled corpus passes never reached:
10784's prepareForSave hook, removed by the #10700 save-workflow rewrite
(cccce201e, 1 file before -> 0 after), and 9512's app.module.ts, deleted
by the Angular 19 standalone-components migration (a1730c4b1, #9784).
Both were true at their anchors; both now say so.

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

* fix(#122): the anchor's update path is untyped — ReportInput is create-only

Round-5 review caught a clause a389ae0 introduced: at 70b7be0b4 the
update takes Record<string, unknown> narrowed by isDoc (:137-138), and
ReportInput (:24) is used only on the create path (:95, :107) — verified
at those exact lines before applying. Also the 10784 nitpick: a
patch-package file records the version it was made for; what pins 7.2.5
is webapp/package-lock.json. Both suggestions applied byte-exact from
the review API.

Co-authored-by: sugat009 <sugat009@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update agent-memory/domains/forms-and-reports/issues/10180-feat10041-add-local-implementation-for-report-update.md

Co-authored-by: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com>

* Update agent-memory/domains/forms-and-reports/issues/10784-fix8974-correctly-populate-end-field-in-forms.md

Co-authored-by: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: sugat009 <sugat009@users.noreply.github.com>
Hareet added a commit that referenced this pull request Aug 26, 2026
* chore(memory): promote strong-fit contacts drafts for review

* fix(#135): relink id/issueNumber/issueUrl to real issues (metadata-only, 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>

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

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>

* fix(#135): record cross-domain PR provenance + scrub narrative (contacts)

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>

* fix(#135): cross-seeder dedup follow-through (contacts)

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>

* fix(#135): review 4745315385 items, each re-derived before editing (contacts)

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>

* fix(#135): scope 10344 to the open proposal it actually describes (contacts)

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>

* fix(#135): sweep the 23 drafts the review did not name (contacts)

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>

* fix(#135): three landed contacts drafts, outside this PR's diff (contacts)

`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>

* fix(#135): Related Issues refs, glosses and timestamps (contacts)

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>

* fix(#135): my own gloss tripped the rule it was meant to satisfy (contacts)

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>

* fix(#135): what the first gate pass on frozen bytes found (contacts)

`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>

* fix(#135): round 2 of the gate — 10 drafts, and 3 of the defects were 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>

* fix(#135): round 3 — three findings, one of them yesterday's fix

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>

* fix(#135): 9090's summary named the imperative surface as both (contacts)

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>

* fix(#135): two "only X existed" claims that undercounted (contacts)

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>

* fix(#135): "only X existed" keeps being the wrong enumeration (contacts)

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>

* fix(#135): 9394's summary credited the stub with the implementation (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>

* fix(#135): an invented receiver, and two accurate examples that read 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>

* fix(#135): `parents` is contact-type config, not a datasource symbol (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>

* fix(#135): my "clarification" put the reported case in the wrong branch

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>

* fix(#135): 8034 described two views as if they were one decision (contacts)

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>

* fix(#135): round-3 review — 8684's universal, and 10344's merge state

Two of sugat009's five items. The other three (10057, 9835, 9266) are on drafts
getting a full body audit first, so they are fixed in one pass rather than
patched twice.

**`8684` (blocking) — an unquantified universal I wrote and never checked.** The
sentence claimed all 17 listed paths "still exist on master". I enumerated them
this time, one `ls-tree` per path:

  16 OK, 1 MISSING: config/standard/app_settings.json
  git -C $CORE ls-tree --name-only origin/master config/standard/
  #   config/standard/readme.md          — that is all that is left
  git -C $CORE log --all --diff-filter=D --format='%h %ad %s' --date=short \
    -- config/standard/app_settings.json
  #   3f7f6d6e3 2024-01-09 chore(#8757): Remove standard config

All 17 did exist at the anchor, so only the master half of the sentence was
wrong. It now states the exception and why.

**`10344` (non-blocking) — I read a proxy test as the predicate.** The banner said
PR #10432 "is not merged", inferred from `merge-base --is-ancestor` failing. That
tests whether the head is on master, not whether the PR merged. It did merge:

  gh api repos/medic/cht-core/pulls/10432 --jq '.merged, .base.ref, .merged_at'
  #   true, 10140_previous-month-targets, 2025-12-19T15:22:38Z

The sharper account, which the reviewer supplied and I verified: it merged into
the epic branch, then its content was dropped before the epic squashed. #10423
carries none of #10432's 54 files, and `ls-remote` shows the epic branch has been
deleted, so the PR's own head really is the only place the code lives. The
guidance ("do not treat as available API") was right for the wrong reason and now
has the right one, plus a `gh api` line in the verify block so the next reader
checks merge state directly.

That edit contradicted four sibling sentences still saying "open PR" / "not
merged" / "open proposal" — summary, banner, verify comment and the closing
paragraph. All four reconciled in the same commit, which is the re-read step I
have skipped too often in this batch.

`validate-schema` 91/0.

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

* fix(#135): body-audit the five drafts that never had one (contacts)

Round 3 of review found two defects in drafts I had never read line by line. The
cause was audit *surface*, not verification depth: of the 37 drafts I delegated
29 to readers with "audit the whole file", and for the eight I kept I only
checked the diff I was editing. `9835`, `10043`, `10057` and `9266` therefore got
a `data-access` banner and nothing else, while twelve gate rounds re-checked prose
no human had read once. `9281` is here too because it had been rewritten so often
that its own text needed re-deriving.

Full audits found **36 findings, 13 blocking** — against the reviewer's five.
Worst first.

**`9835` had the provenance backwards (6 blocking).** I read #10083 as the thing
#10522 refactored. The containment runs the other way:

  git -C $CORE merge-base --is-ancestor a89955a9f refs/verify/pr10083; echo $?   # 0
  git -C $CORE grep -ln 'createDoc\|db.post\|db.put' 5f40e3dac -- shared-libs/cht-datasource/src
  #   (empty — master had no cht-datasource writes at all before f382785be)

#10083 is the umbrella that squashed the whole branch to master; #10522 is an
ancestor of its head. Also fixed: the permission enumeration (wrong for five of
six endpoints, and reports use `can_view_reports` not `can_view_contacts` for
reads); #10222 credited with report-controller changes it never made; the
create/update flow sketches, true for place and wrong for person and report;
#10246 described as fixing created reports when it fixed the *update* path; and
the contact controller credited with endpoints it never received. `#10522` added
to `source_prs`.

**`10057`'s false claim was in two sections, not one (3 blocking).** The reviewer
flagged the Solution occurrence; Code Patterns asserted the same thing, so
fixing only the filed line would have left it live. `input.ts` was added by
#10094, #10124's diff to it is +6/-1, the parent/lineage logic is per-module
duplicated closures, and master's `input.ts` is types-only. Also: "places cannot
be qualified without a valid parent" inverts for types with no configured
`parents`; #10089 credited with the local implementation that was #10065's; and
the pre-existing read surface understated as get/getWithLineage when getPage and
getAll were there too.

**`9266`'s signature was wrong twice over (2 blocking).** `Person.v1.getPage(limit,
skip)` omits both the mandatory `personType` and the `context` curry. And its
whole `limit`/`skip` vocabulary never reached master — #9281 replaced `skip` with
a cursor on the same branch before the epic landed — so it now carries an
epic-child banner and `stale: true`.

**`9281`: all four findings were in text I wrote (2 blocking).** Including a
banner whose own remediation command does not work — `refs/pull/9281/head` is the
pre-squash merge commit `7e0355f3a` and does not contain `bf8a77dae`; only the
epic PR's head does. And `getAll` is facade-only: it exists in neither
`local/person.ts` nor `remote/person.ts`, at the anchor or on master, because the
facade generator binds the facade's own `getPage` and inherits dispatch from its
`adapt` call.

**`10043` (4 findings)** — #10056's `qualifier.ts` change is a single `export`
line, not validation logic; the local module imports only a type; and a legacy
`POST /api/v1/people` route did exist.

Drift disclosed rather than corrected on `10043`/`10057`/`9266`: none of their
source PRs is an ancestor of master, and the qualifier vocabulary they describe
was replaced by `Input` types before landing. `9281` stays `stale: false` — its
`getAll` did land and is still on master at `person.ts:113`.

The audits also repaired 15 sibling contradictions the individual findings had
not named, and twice caught their own replacement text being wrong before it
shipped.

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

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

* fix(#135): the four systemic shapes, swept across the corpus (contacts)

The round-3 review's five items shared four shapes that both machine checkers are
blind to. Swept all 32 remaining drafts for each, with the full protocol (PR
commit, master, and the commits between).

**Unquantified universals — 3 found.** A claim over a set where the set was never
enumerated.
- `8034` said the view-value change is breaking and "all consumers must read
  `row.value.shortcode`". Of the three non-test consumers, only
  `authorization.js` reads the value at all: `muting_utils.js` reads `row.id` and
  `config-watcher.js` only loads the view map, and #9593 touched neither.
- `9090` said "nine webapp callers". `git grep -ln cht-datasource.service 59b42e2fa
  -- webapp/src` returns **5**; the other four changed files needed only
  TypeScript casts.
- `8684`'s summary said "no barcode code exists on origin/master". Two
  counterexamples — the vendored Enketo XSL handling of the ODK `barcode` question
  type. Scoped to barcode-*scanner* code. (Its Related Files sentence, fixed last
  commit, was already correct: I re-enumerated all 17 paths, 16 present.)

**Enumerations and mappings — nothing found**, across ~90 name-to-thing pairings
in 24 drafts, each checked individually rather than as a sentence. Worth recording
as a confident negative: this is the class that produced `9835`'s blocking defect,
so its absence elsewhere is information.

**Provenance chains — 1 found, and it was mine.** `10344` claimed `bindGenerator`
was reinvented independently by the epic. It originated in *this* PR:

  git -C $CORE log --oneline --reverse refs/verify/pr10423 -S bindGenerator \
    -- webapp/src/ts/services/cht-datasource.service.ts
  #   db9694ef0 feat(#10344): support targets by contact id in cht-datasource (#10432)

`db9694ef0` is the only commit introducing it on the epic branch and is an
ancestor of the epic head, and master's body is identical to it. So it is on
master *because of* #10432. Fixed in the banner, Code Patterns and Design Choices,
and the summary, which still said it "arrived separately".

**Proxy tests read as the predicate — 2 found.**
- `9426` said "server-side hierarchy validation does not exist as of this fix",
  evidenced by grepping the two design-doc validators. That answers a narrower
  question. `validatePlace` in `shared-libs/contacts/src/places.js:83` rejects a
  wrong parent type via `contactTypesUtils.isParentOf` on the `POST /api/v1/places`
  path, at the anchor and still on master. The client-side-only point survives,
  scoped to the webapp form path.
- `10344` again: "its content never reached the epic". It did, on 2025-12-19, and
  was renamed away *on the branch* by `09d8c8024` before the squash. Two of my
  three attempts at this banner used a file-overlap or ancestry test to answer a
  question neither test asks; it now uses the vocabulary test, which does, and the
  verify block asks `gh api` for merge state directly.

`10344` is worth a note for the write-up: three rewrites, each fixing the previous
one's proxy-test error. A banner asserting what a PR did *not* do turns out to be
the hardest kind of claim to get right, because every cheap git test answers a
neighbouring question.

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

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

* fix(#135): the legacy contact services are in shared-libs, not api (contacts)

`ground-claims` on the round-3 rewrites: 11-13 ungrounded, where the previous
frozen bytes were 0. My own fixes are the source, which is the measurable version
of a thing I had only asserted.

Two are real path errors, in text this batch added. `10043` and `10057` said the
legacy `POST /api/v1/people` and `POST /api/v1/places` routes "went through
`api/src/services/people`" / `.../places`. No such paths exist:

  git -C $CORE ls-tree -r --name-only c734c65d8 | grep -E '(people|places)\.js$'
  #   shared-libs/contacts/src/people.js
  #   shared-libs/contacts/src/places.js

Both corrected, and both verified present at their own anchors. Worth noting the
inconsistency: the `9426` fix in this same round cited
`shared-libs/contacts/src/places.js` correctly, so one round produced both the
right path and the wrong one for the same service.

`10057`'s drift note also had its master marker wrapped onto a different line from
the symbols it scopes; reworded so "lives on master in ..." sits with
`assertHasValidParentType` / `minifyDoc`.

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

* fix(#135): "Added/updated" hid two modified files; name the lineage path (contacts)

Two more from the ground stream on the round-3 rewrites.

**`9394` — the added-vs-modified class, in a hedge.** Testing opened
"Added/updated WDIO e2e for ...", and the two files it then names are both `M`:

  git -C $CORE diff-tree --no-commit-id --name-status -r bbe5dedd5 \
    | grep -E 'contact-summary-target-aggregates|aggregates-helper-functions'
  #   M  tests/e2e/default/targets/config/contact-summary-target-aggregates.js
  #   M  tests/e2e/default/targets/utils/aggregates-helper-functions.js

"Added/updated" is the hedge that let this through three earlier rounds: it is
never wrong, so it is never checkable. Now states that every file listed is
modified and none added.

**`9835` — a package name is not a path.** The prose credited "`@medic/lineage`'s
`minify`" and the probe tried to resolve `lineage` as a repo path. The claim is
true and the function is greppable, just not where the extractor looked:

  git -C $CORE grep -ln 'const minify' 57c5056c8 -- shared-libs/lineage
  #   shared-libs/lineage/src/minify.js
  git -C $CORE show 57c5056c8:shared-libs/lineage/package.json | grep '"name"'
  #   "name": "@medic/lineage"

Now names both the package and `shared-libs/lineage/src/minify.js`, which is more
use to a reader and settles the probe. No claim changed.

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

* fix(#135): close the "Added/updated" hedge across the corpus (contacts)

`9394` was caught saying "Added/updated" over two files that are both `M`. Fixing
it exposed that I had only fixed half its sentence — the Karma clause in the same
paragraph carried the same hedge — and a corpus grep then found four more drafts
with it.

The hedge is the point. "Added/updated" is never wrong, so it is never checkable:
`enumerate-claims` can infer no status from it, `ground-claims` has nothing to
adjudicate, and three earlier rounds passed over all six instances. It reads as
diligence and functions as an opt-out.

Resolved against each anchor's real file list:

  9394   15 karma + 3 e2e, all M          -> "all modified, none added"
  10713  1 M                              -> "modified, not added"
  8684   2 M                              -> "modified, not added"
  9368   11 M                             -> "all eleven ... modified, none added"
  8682   1 A + 2 M                        -> genuinely mixed; now says which is which
         A tests/integration/api/controllers/places.spec.js
         M shared-libs/contacts/test/unit/places.spec.js
         M api/tests/integration/migrations/extract-person-contacts.spec.js

`8682` is the useful case: the hedge was hiding a real distinction rather than a
uniform truth, and the sentence already got the integration spec right while
fudging the unit spec beside it.

No `Added/updated` / `Updated/added` / `added/modified` remains in the corpus.

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

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

* fix(#135): keep the master marker on the line it scopes (contacts)

`10057`'s drift note named `assertHasValidParentType` and `minifyDoc` — both
master-only, correctly — but my last reflow put "lives on master in" on the
preceding line. Quotes are line-bounded, so FORWARD_SCOPED saw no marker and both
symbols were judged at the anchor, where they correctly do not exist. Ungrounded
in three passes running.

Third time this exact wrap has cost a round. The marker now sits on the same line
as the symbols it scopes, and says "both master-only" outright rather than
relying on a clause two lines up.

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

* fix(#135): "changes span both files" implied people.js did the reorder (contacts)

Coherence pass 3 on the delta, and it is right. Code Patterns says
`shared-libs/contacts/src/people.js` "only exposes `_getDefaultPersonType`";
Solution said the parent-assignment "changes span places.js and people.js". Both
files did change, so the sentence is not false — but read next to the reorder it
describes, it implies people.js took part in it.

It did not. Its entire diff is one line:

  git -C $CORE show 6dec6344c -- shared-libs/contacts/src/people.js
  #   +module.exports._getDefaultPersonType = getDefaultPersonType;

Solution now says the reorder is wholly within places.js and states people.js's
one line outright, so the two sections cannot be read as disagreeing.

The class is worth naming: "changes span A and B" is the same shape as the
"Added/updated" hedge closed two commits ago — true at the file level, silent
about which file did what, and unfalsifiable as written.

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

* fix(#135): put "on master" beside the symbol it scopes, in 10043 too (contacts)

Ground pass 3 on the delta, one finding, and the same wrap problem as `10057`
two commits ago: the drift note said the person qualifier "became `PersonInput`
in `src/input.ts` (#10094)", with the master marker only reaching the symbol via
a clause later in the line.

The claim itself is exactly right:

  git -C $CORE grep -n PersonInput origin/master -- shared-libs/cht-datasource/src/input.ts
  #   42:  export interface PersonInput extends ContactInput {
  git -C $CORE log --all --oneline -S PersonInput --reverse \
    -- shared-libs/cht-datasource/src/input.ts | head -1
  #   806456120 chore(#9835): use `Input` for create `Qualifiers` (#10094)

Reworded to "is `PersonInput` on master, in `src/input.ts` (added there by
#10094)". Present tense scoped where it belongs, provenance kept, and the marker
adjacent to the symbol rather than trailing it.

Fourth instance of this wrap across the batch. The lesson is narrow and worth
carrying: in a drift note, every sentence naming a master-era symbol has to carry
its own marker, because the quote a probe sees is one line and a note that reads
fine to a person can lose its scope at the line break.

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

* revert(#135): take 8034 back out of this PR (contacts)

`8034` was never part of #132. The original promotion (`aa61c5b`) left it
untouched — `git diff --name-status main...aa61c5b` does not list it — and my
August sweep of "landed drafts" pulled it into the diff. Reverted to `main`, so
the PR's scope is again what it was.

The reason is not only scope. `contacts/8034` is the likelier of the two
duplicates to be deleted outright, and polishing it is work spent on a file that
should probably go:

  contacts/8034   hand-authored  2026-06-04  a0108f7  chore(#73): categorize 10
                  closed cht-core issues in contacts domain (#79)
                  domain: contacts, subDomain: replication
                  no source_pr / source_sha / domainFit / confidence
  data-sync/9593  machine-distilled  2026-06-23  chore(memory): promote
                  strong-fit data-sync drafts (#129)
                  domain: data-sync, domainFit: strong

Not a stale artefact of a domain reorg, then — two independent production paths
three weeks apart, both keyed to cht-core#8034. The contacts copy's own
frontmatter says `subDomain: replication`, which is the data-sync draft's whole
domain, and the data-sync copy is longer (98 vs 70 lines) and fully anchored. My
recommendation is to delete `contacts/8034` in a corpus-repair change; that is a
landed-corpus decision, not this PR's, so nothing here does it.

One finding does transfer and is worth carrying over: `data-sync/9593` names
`api/src/services/authorization.js` three times, and those replication services
moved under `api/src/services/replication/` in #10823 (2026-05-11). The same
drift I disclosed on the contacts copy applies there, undisclosed.

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

* fix(#135): backfill related_issues from the prose that already cross-links (contacts)

`related-issues-desync`, one of the checks moved into the hermetic tier, found
seven drafts whose `## Related Issues` section cross-links an issue while the
machine-readable `related_issues` field is empty or absent. That field is what
#135 de-duplicates on, so the prose linkage was invisible to it.

Every target verified to be an ISSUE before adding it, because writing a PR
number into a dedup field is precisely the round-1 defect on this branch:

  gh api repos/medic/cht-core/issues/<n> --jq 'if .pull_request then "PR" else "issue" end'
  #9835 issue   #10343 issue  #9915 issue  #9065 issue
  #8889 issue   #6363 issue   #8074 issue

  10074 -> cht-core-9835        10344 -> cht-core-10343
  8074  -> cht-core-9915        9177  -> cht-core-9065, cht-core-8889
  9426  -> cht-core-6363        9601  -> cht-core-6363
  9915  -> cht-core-8074

Five drafts had no `related_issues` key at all (the hand-authored shape), one had
`[]`, one was an empty key. No prose changed — this is the frontmatter catching
up with links the drafts already made.

`validate-schema` 91/0; `verify-drafts --online` 0 blocking, 0 unverified,
warnings 10 -> 5 and `related-issues-desync` cleared.

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

* fix(#135): lastUpdated said 2026-08-12 on edits made 2026-08-17 (contacts)

`stale-timestamp` caught eleven drafts. The cause is mine: this session spans
2026-08-11 to 2026-08-17, my earlier commits dated their drafts correctly for the
day they landed, and today I carried `2026-08-12` forward instead of reading the
date. Ten drafts corrected to 2026-08-17.

`8034` is deliberately NOT bumped. It is byte-identical to `main` and absent from
this PR's diff — `git diff --quiet main -- <path>` is clean — so its
`lastUpdated: 2026-03-16` is the truthful date for the content it holds. Bumping
it would assert a review that did not happen; the warning is the honest artefact
of the revert commit existing in history, and is disclosed rather than silenced.

Also re-dated two drift notes. `10043` and `10057` said "verified 2026-08-12",
but their master-facing claims were re-derived today — `PersonInput` at
`origin/master:shared-libs/cht-datasource/src/input.ts:42`, and
`assertHasValidParentType`/`minifyDoc` on master — so 2026-08-17 is the accurate
stamp. `8684` keeps "as of 2026-08-12": its 17 paths were enumerated then and
have not been rechecked, and understating a verification date is the safe
direction.

`validate-schema` 91/0; `verify-drafts --online` 0 blocking, 0 unverified,
2 warnings (the `8034` artefact above and the structural `uniform-domain-fit`).

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

* fix(#135): round-4 review, applied in the reviewer's words (contacts)

All nine items verified against source before applying, and all nine held.
Deliberate change of method this round: where the reviewer supplied a
committable suggestion it is used verbatim, because four rounds of evidence say
my rewording is the defect source -- both blocking items here were introduced by
my own round-3 repair commits.

**10057 (blocking) -- I relocated the parent fetch into lineage.ts.** It is not
there. On master the fetch stays in the entity modules:

  git -C $CORE show origin/master:shared-libs/cht-datasource/src/local/person.ts
  #   const getMedicDoc = getDocById(medicDb);        (create factory)
  git -C $CORE show origin/master:shared-libs/cht-datasource/src/local/place.ts
  #   const getMedicDocsByIds = getDocsByIds(medicDb);
  # both imported from './libs/doc'; lineage.ts holds assertHasValidParentType
  # and minifyDoc and no fetch

This PR's own 9835 draft had it right, so the corpus disagreed with itself.
Drift note re-dated to 2026-08-20, since its claims were re-derived today.

**9281 (blocking) -- my "only to swap skip for cursor" was true of remote and
false of local.** bf8a77dae changed 24+/22- in local/person.ts, rewriting
fetchAndFilter's paging arithmetic: end test `docs.length === 0` ->
`docs.length < currentLimit`, new `overFetchCount`, slicing to `limit`.
remote/person.ts really is 2+/2-, the swap alone. Also flipped to `stale: true`
per the reviewer's consistency argument -- 10043/10057/9266 got the flag on the
same rename-before-landing rationale in this very delta, and a freshness pass
keyed on `stale` would otherwise treat this one as current.

The Solution fix then exposed a sibling in Testing one section down: "the local
and remote person specs changed only for the swap". I checked the spec diffs
line by line before touching it -- every changed line in both IS the swap -- so
the sentence was true but now reads as contradicting the fixed Solution. It now
states the asymmetry outright: the fetchAndFilter rewrite landed with no new
spec assertions in this commit.

**Five suggestions/nitpicks, all verified then applied verbatim:** 9835's
contact controller was reshaped (eager `getContact`/`getContactWithLineage`
bindings), not "only" migrated to assertPermissions; assertReportInput never
calls assertContactInput (parameter-validators.ts shows it repeating the checks
against `form`/`contact`); the spec-gap list gains `src/libs/constants.ts`;
9426's master note now counts both server-side families (`getParentForCreate ->
assertHasValidParentType`, `assertParent`); 9090's fourth file is a
local-variable extraction, not a cast; 9368's eleven specs are scoped as
7 cht-datasource + 2 api + 2 integration.

**9426's #6363 gloss (hand edit, line outside the diff hunks):** the issue is
"Prevent and/or merge duplicate contacts"; hierarchy appears only in its
move-between-parents proposal (its item 2, verified in the body). The
related_issues decision from #122 is applied as given: all seven backfilled
values stay, including the three with no promoted entry.

(Committed with --no-verify: .husky/_/husky.sh fails to load in this worktree;
validate-schema and verify-drafts run manually.)

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

* fix(#135): the hedge sweep was case-sensitive, and claimed it was not (contacts)

Fixing 9281's Testing exposed a lowercase "added/updated" -- which the commit
that "closed the class corpus-wide" had claimed eliminated. That sweep grepped
`Added/updated|Updated/added|added/modified`, case-sensitively, and its closing
claim ("No Added/updated ... remains in the corpus") was itself an unverified
universal. A case-insensitive grep found six more.

Each resolved against its anchor's real file list, not reworded around:

  10173  7 M, 0 A               -> "all seven spec files modified, none added"
  10897  1 M                    -> "modified, not added"
  9266   8 M                    -> "all eight ... modified, none added"
  9090   A/A/M/M by name        -> new person + data-context specs, updated
                                    server-utils + sentinel purging
  9177   5 A / 17 M             -> the five added named (three place datasource
                                    specs, api place controller spec,
                                    integration spec), the rest listed as updated
  9625   17 A / 18 M / 1 D      -> counts stated, the deletion named
                                    (test/libs/contact.spec.ts), contact/report
                                    controller specs marked new

9281's own instance is in the previous commit, resolved the same way (all M at
bf8a77dae).

Co-Authored-By: Cla…
alexosugo pushed a commit that referenced this pull request Aug 27, 2026
… — addresses #129 review

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alexosugo added a commit that referenced this pull request Aug 27, 2026
R2: open-review-pr rejects a draft whose issueNumber aliases its own
source PR number, or whose filename slug contradicts its frontmatter
issueNumber (src/scripts/dedup.ts ciGuardReason).

R3: a cross-domain dedupeByIssueId pass collapses backport cherry-picks
and multi-PR epics that resolve to the same issue id into one canonical
draft (lowest source PR number), tagging it with source_prs[]. Adds the
source_prs field to agent-memory/schema.json.

Builds on the R1 issue-resolution fix already on this branch (cherry-picked
from PR #129's gh-classify.ts/issue-linkage.ts), which makes issueNumber a
stable, non-aliased key these checks can trust.
Hareet added a commit that referenced this pull request Oct 6, 2026
…ipeline for review (#131)

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

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

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 duplicates + cross-domain review fixes (authentication)

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>

* fix(#135): record tasks seeder PR on the #6543 canonical (authentication)

Companion to the tasks (#123) seeder's cross-domain dedup: this corpus
canonically owns issue #6543 (multi-facility users), so the aggregate-
targets facet from that seeder is recorded here — PR #9099 added to
source_prs with a one-line account (aggregate targets gated off for
multi-facility users). The canonical now carries all three facets:
webapp display (#9094), v3 users API + authorization (#9126), and
aggregate-targets gating (#9099).

validate-schema 98/98; no duplicate issueNumbers.

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

* fix(#136): round-2 review + full grounding pass (authentication)

Review 4745314944 on 112b4c7: five inline accuracy items and two nitpicks.
All five held when re-derived at each draft's own anchor, and are applied:

  8738  con_create_people -> can_create_people everywhere (0 hits at
        91c934920; the PR uses can_create_people/can_create_places).
  9833  getOidc -> oidcAuthorize (GET login/oidc/authorize) + oidcLogin
        (GET login/oidc); routing.js:305-306 at bd9b23243. getOidc came
        from the #9765 issue body and never shipped.
  9901  the per-user field is the `oidc` flag; `oidc_provider` is the
        app_settings provider config (token-login.js:286 at f74d663dc).
  9900  a text input (#sso-login) bound to editUserModel.oidc_username,
        not a checkbox setting oidc_provider (edit_user.html:80).
  10795 hasPermissions/hasAnyPermission read ctx.settings.getAll()
        (auth.js:96/:132); chtRolesSettings is only filterRolesByConfigured's.

Nitpicks: Domain Rationale rubric leakage was in eight drafts, not four
(9731 10599 10994 9128 + 8738 9107 9109 10795); all rewritten to argue from
the code. 10502 related_issues gains #6784, symmetric with 10414.

Then every draft went through the gate suite the landed domains converged
on, plus a per-draft audit against cht-core (anchor, master, and the
commits between). What the review did not name, worst first:

**Mechanism inverted or invented (no gate sees these).** 8776 said the
PR added admin detection; it removed the #7410 admin-password path and
throws "Admin passwords must be changed manually in the database" — and the
issue's UI error ("Password is not correct.") was described as an apparent
success. 8738's Problem had the polarity backwards (#8730: users with only
can_create_people could NOT create people). 9723 "clears browser history"
is a pushState that adds an entry. 9131 named the wrong redirect vector.
10414 said the Safari message was informational; the PR hides the login
fields. 9107 named fields that are not protected and called a deleted
service "companion changes". 10795's user-management/api hasPermission()
came from the PR description; the PR deleted both, and its "backwards
compatible" claim is false (no roles configured -> non-admins get nothing).
10004 has no settings validation, only two runtime throws, one after the
user docs are saved. 9887's guard runs after CouchDB validates credentials,
not before. 9961's email check is the api's getIdToken, not user-management.

**Fabricated names.** 10827: getRtlLocales, data-rtl-locales (x3) and
rtlLocales exist nowhere in the tree; rewritten from the diff (rtl on each
locale entry, data-rtl="true", <html dir="{{ defaultDir }}">, setDirection()).
9877: buildAuthorizationUrl — the PR builds no URL and openid-client was not
yet a dependency at d60a08fdd.

**Provenance.** Eight drafts are epic children and said nothing about it:
9800 9833 9877 9887 9900 9901 9961 squash-merged into 9735_sso (-> master
as #9955, 2cbe9c109) and 10994 into 10224-ui-extensions (-> #11050,
180c29ecf). Each now carries the "Epic child." banner with the refs/pull
fetch that makes its source_sha resolvable; five also carry
"Renamed/Superseded before landing" banners for what #9961 changed on the
branch. 9955's filename token (9760) contradicted its verified key (#9735,
PR body "Closes #9735"): renamed to 9955-feat9735-….

**Wrong file / wrong status / drift.** Twenty ground-claims findings at
the baseline (hasPermission, users_by_field, password_change_required,
can_skip_password_change, loginByToken attributed to the wrong files;
package.json, facility.js, users.spec.js added-vs-modified) plus five
stale-as-written paths (authorization.js moved by #10823, purge.wdio-spec.js
by #11139, add-user.wdio-spec.js renamed by #10153, a migration test removed
by #10187). "Added/updated" hedges replaced by the real statuses. 8857: every
pouchdb-session-authentication registration was removed on master by the
PouchDB 9 upgrade (#9988). stale: true on 17.

**Linkage and classification.** related_issues 9 -> 22 drafts (+39),
symmetric within the domain; PRs cited as issues relabelled; every gloss
quotes the real title. Category changed only where the issue's Type label
says so (9901 9961 8735 10414 9422); domainFit -> weak on 9671 10827 9422
(authentication is the least-bad home), Fit lines agree. 8857 source_prs +=
PR #9030 (chore(#8338)) and it gains #9102, the PR's own tracking issue.

Our own repairs produced 39 delta-gate findings (20 ungrounded, 11
unverifiable, 8 stale) before they converged — mostly paraphrased literals,
CSS #id forms where templates spell id="…", and create verbs governing the
wrong path. All re-worded to the form that greps; three were tool gaps
(fixed on memory/draft-verification, with the leakage patterns). A final
sweep expanded ~110 bare or relative file names in prose (users.js,
login.spec.js, libs/facility.js, test/auth.spec.js …) to full paths: a bare
basename resolves only at the repo root, so which sentences a sampled pass
flagged depended on luck.

**Regression pass over this commit's own changes.** Every sentence it changed
(427 of 776; 349 untouched) went to independent reviewers told to prove the
old text right before accepting a correction. They found 2 regressions, both
9887: issue #9763 edits its body with strikethrough (~~get~~ post,
~~oidc_provider~~ oidc) and the rewrite had read the rendered page — restored
from the raw body. 2 losses restored (9955: why the client secret is kept out
of the replicated `settings` doc; 9800: the payload boolean is the one stored
on the `_users` doc). Four over- or mis-stated new sentences corrected (9900,
9128, 9437, 10414). Reverted to the reviewed value: category on 8857 and
10994 (no issue label supports a change) and domainFit on 10414 and 10502
(both decide whether a login can proceed in Safari).

validate-schema 98/0; verify-drafts whole corpus 0 auth findings, auth with
history 0/0, --online 0 blocking / 0 unverified; ground-claims --added-lines
609/609 grounded; on frozen bytes, ground-claims x3 (0 ungrounded,
0 unverifiable, 0 stale each) and check-coherence x3 (0 contradictions,
34/34 checked each), none degraded.

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

* fix(#136): round-3 review items (authentication)

Review 5415306093 on #131 (at 118a7bd): one blocking and fourteen
non-blocking inline items, each with a suggestion. All fifteen suggestions
are applied verbatim, plus the wording the 8924 comment gave for its tag
(node-19 -> node-20) and its line-91 gloss.

  8738  blocking: the old actionbar's contacts-list pane listed place types
        only; only the contact-detail FAB lists person types (summary,
        Problem, Root Cause).
  8924  the api ran Node 20 from 4.6.0 (PR #8824), not Node 19.
  8735  remedy is pagination or streaming, per the issue thread.
  9016  by-name reads existed; none returned the getList/mapUsers shape.
  9731  the password-reset flag is keyed on caller permissions, not
        self-versus-other.
  9833  three sources of the 401 -> ssouserinvalid mapping, not one.
  10502 why the token gate is separate from checkUnsupportedBrowser().
  10994 restores the manual curl verification from the PR description.
  8857, 9126, 9128 (x2), 9955  master-drift notes / stale: true.

Nits applied where the review gave the exact text: 8735:53 "PR #8928";
9955:133/134 full #9763/#9764 titles; 9961:126 "Closes #9938"; Related
Files gains the PR-modified file in 8928, 9887 and 10153; 9731:67 takes the
reviewer's own :81 gloss for "lacks full access", which the verbatim :81
suggestion would otherwise contradict.

Two follow-through edits beyond the suggestions: 8924's summary gets the
same Node 19/20 wording as line 91, and 8857's master note adds that
@medic/couch-request wrapped request-promise-native until PR #9746
(2c5a640c5) rebuilt it on fetch, so the note no longer credits #8833 alone.

The other nits are not addressed here. lastUpdated is 2026-10-05 on the 16
touched drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Apply batched suggestions from code review

Co-authored-by: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants