Skip to content

chore(memory): promote strong-fit tasks-and-targets drafts from memory-pipeline for review - #123

Merged
Hareet merged 26 commits into
mainfrom
memory/promote-tasks-and-targets
Aug 11, 2026
Merged

chore(memory): promote strong-fit tasks-and-targets drafts from memory-pipeline for review#123
Hareet merged 26 commits into
mainfrom
memory/promote-tasks-and-targets

Conversation

@Hareet

@Hareet Hareet commented Jun 24, 2026

Copy link
Copy Markdown
Member

Promotes 35 strong-fit tasks-and-targets drafts from agent-memory/_pending/ into agent-memory/domains/tasks-and-targets/issues/ for squad content review.

Categories: feature (21), bug (9), improvement (5)
Themes: rules engine (banish nools, stale-state migration), aggregate targets & filters, task filtering/telemetry, reporting periods.

All 35 carry domainFit: strong + a ## Domain Rationale section. 2 weak-fit drafts deferred to Stream C.

@Hareet
Hareet requested a review from sugat009 June 24, 2026 05:20
@alexosugo alexosugo closed this in 951d82d Jun 24, 2026
alexosugo added a commit that referenced this pull request Jun 24, 2026
…, CLI, and CI workflow (#128)

* feat(#108): add pipeline directory scaffolding and types

- Create agent-memory/_pending/<domain>/ for all 8 CHT domains
- Add empty agent-memory/_skipped.ndjson for filter audit log
- Define pipeline types in src/types/pipeline.ts: ScrapedPR,
  LinkedIssue, ReviewComment, FilterDecision, SkipLogEntry,
  ScraperError

* feat(#108): implement PR scraper using gh CLI

Synchronous scraper that collects structured data from medic/cht-core
PRs for the memory distillation pipeline.

- Fetches PR metadata, diff, review summaries, and linked issues
- Uses execFileSync with array args (no shell injection risk)
- 50 MB maxBuffer with ENOBUFS → ScraperError
- Graceful fallback for org-membership (read:org not required)
- Linked issues extracted via regex; GitHub sidebar links documented
  as a known limitation (requires GraphQL)
- PENDING reviews filtered out
- 22 unit tests, all coverage thresholds met

* feat(#108): extend schema with provenance fields and add validator

- Add 10 optional frontmatter fields to schema.json: source_pr,
  source_sha, distilled_at, reviewed_by, reviewed_at, confidence,
  entities, concepts, related_issues, stale
- All new fields are optional (backward-compatible with existing entries)
- Update TEMPLATE.md with commented stubs and field reference table
- Add src/scripts/validate-schema.ts (gray-matter + ajv) to validate
  all agent-memory /**/*.md frontmatter; 30/30 existing entries pass
- Add npm run validate-schema script
- Fix YAML syntax error in 10729-smsparser-bugs-typos.md (unquoted colon)

* fix(#108): extend linked-issue regex to match full GitHub issue URLs

Bare keyword regex (e.g. "Fixes #123") missed the URL form used by
some cht-core PRs ("Fixes https://github.com/…/issues/123").
Updated pattern captures both forms and deduplicates across them.
Added 2 tests: URL extraction and bare+URL dedup.

* feat(#108): add filter stage with deterministic rules and LLM triage

Three-stage pipeline filter for ScrapedPR objects:
- Stage 1: 5 deterministic skip rules
  (bot, revert, chore/docs/ci/build, lockfile-only, translation-only)
- Stage 2: 3 deterministic distill rules
  (bug+issue+multiservice, feature+issue, shared-libs+multiconsumer)
- Stage 3: LLM triage via ChatAnthropic with Zod structured output;
  graceful fallback to flag-for-human on failure or missing API key

Also adds ScrapedPR.author field, FilterResult/FilterOptions types,
and _skipped.ndjson audit log (append-only, skip/flag decisions only).

DoD items verified:
- All 5 skip rules + all 3 distill rules implemented and tested
- touchesMultipleServices edge cases covered
- LLM failure -> flag-for-human, no throw
- skipLlm option bypasses LLM call
- 211 tests passing, branches 89.24%, lines 99.29%

* feat(#108): add OpenRouter support with Anthropic fallback to filter triage

Switch LLM triage from hardcoded ChatAnthropic to a dual-provider setup:
- OpenRouter (OPENROUTER_API_KEY) as primary via ChatOpenAI
- Anthropic direct (ANTHROPIC_API_KEY) as fallback
- Graceful flag-for-human when neither key is set

Also extracts DEFAULT_TRIAGE_MODEL constant and caches the triage chain
via a module-level singleton (getTriageChain) to avoid re-construction
on every PR in a batch run.

TRIAGE_MODEL env var overrides the default (anthropic/claude-haiku-4).

* fix(#108): correct OpenRouter model ID and add smoke-test script

'anthropic/claude-haiku-4' is not a valid OpenRouter model ID — updated
DEFAULT_TRIAGE_MODEL to 'anthropic/claude-haiku-4-5'. Also adds
smoke-test.ts for manual end-to-end pipeline validation.

* feat(#108): add distiller stage — LLM-powered knowledge draft generator

Implements distillPR() which takes a ScrapedPR that passed the filter
stage and produces a schema-valid markdown draft in agent-memory/_pending/<domain>/.

- Structured LLM output via Zod (domain enum, required fields) with
  OpenRouter-primary / Anthropic fallback, matching filter.ts pattern
- Deterministic markdown assembly in code; AJV validates frontmatter
  before the file is written
- distillFn injection option for tests (no real API calls in test suite)
- Graceful degradation: LLM failure → flag-for-human + _skipped.ndjson
- 25 tests, 86.31% branch coverage, all thresholds met

* refactor(#108): simplify distiller — fix bugs, extract shared constants, async I/O

- Fix double-wrapped error prefix in distillPR catch block (was emitting
  "Distill LLM unavailable: Distill LLM unavailable: ..." on the no-key path)
- Replace sync fs.*Sync calls with await fs.promises.* so batch processing
  does not block the event loop
- Fix .slice-before-filter on review comments so non-empty reviews beyond
  index 2 are not silently dropped
- Derive CHT_DOMAINS list in buildPrompt from the constant rather than a
  separate hardcoded string that could drift
- Extract CHT_DOMAINS, DEFAULT_PIPELINE_LOG_PATH, DEFAULT_PIPELINE_OUTPUT_DIR
  to src/constants/index.ts; both filter.ts and distiller.ts now import them
- Switch process.env bracket notation to dot notation in both scripts
- Remove redundant `const parsed = matter` alias in distiller.spec.ts

* feat(#108): extend smoke-test to run distiller stage end-to-end

Adds distillPR call after a distill decision so smoke-test validates
the full scrape → filter → distill pipeline in one run.

* test: reproduce bug — second call with same draft content rejected as missing frontmatter

* fix: replace gray-matter .matter property check with hasFrontmatter() string check

gray-matter v4 caches parsed results in matter.cache[input]. When the same
content string is parsed a second time the cached object is returned, which
may not retain the 'matter' non-enumerable property. Checking the raw string
with hasFrontmatter() before calling matter() is deterministic and cache-
independent, making all apply-mode tests reliable.

* fix: use dot notation for Record<string,unknown> property access

* feat(#108): add OpenReviewOptions/ReviewPRResult types and open-review-pr npm script

* docs(#108): document OPENROUTER_API_KEY, TRIAGE_MODEL, DISTILL_MODEL in .env.example

* chore: update skipped log with smoke-test run entries

* refactor(#108): extract shared schema-utils and clean up open-review-pr

- Extract buildValidator, normalizeFrontmatter, hasFrontmatter, REPO_ROOT,
  SCHEMA_PATH into src/scripts/schema-utils.ts; import from both
  open-review-pr.ts and validate-schema.ts to eliminate duplication
- Fix buggy parsed.matter check in validate-schema.ts to use hasFrontmatter()
- Hoist buildValidator() call to module scope in open-review-pr.ts so schema
  is compiled once per process, not on every openReviewPR() invocation
- Replace existsSync+readdirSync TOCTOU pattern with single readdirSync+catch
- Deduplicate path.basename call in writeSkipEntry
- Bound uniqueBranchName loop to 99 iterations with explicit throw on exhaust
- Remove phase 1/2/3 what-not-why comments

* feat(#108): add run-pipeline CLI, GitHub Actions workflow, and skipped log update

- src/scripts/run-pipeline.ts: CLI entry point wiring Scraper → Filter → Distiller
  for single PR (--pr) or recent window (--since / default 24h)
- .github/workflows/run-pipeline.yml: daily cron (06:00 UTC) + manual dispatch
  that runs the pipeline and commits knowledge drafts back to the branch
- package.json: add run-pipeline npm script
- agent-memory/_skipped.ndjson: update with latest smoke-test run entries

* fix(#108): resolve CI lint failures — promote ajv/gray-matter to deps, fix auto-fixable errors

- Move ajv, ajv-formats, gray-matter from devDependencies to dependencies
  so n/no-unpublished-require passes for src/scripts/ files
- Add missing ajv to dependencies (only ajv-formats was listed)
- Run eslint --fix to correct dot-notation and indent violations
- Remove unused callCount variable in open-review-pr.spec.ts
- Remove invalid @typescript-eslint/no-throw-literal eslint-disable
  comments in scraper.spec.ts (rule was renamed in typescript-eslint v6+)

* test(#108): add branch-coverage tests to meet 85% threshold

- Add normalizeFrontmatter lastUpdated→last_updated aliasing test
  (covers schema-utils.ts lines 49-50)
- Add non-Error LLM throw test in distillPR
  (covers distiller.ts line 275 false branch)
- Add empty linkedIssues prompt test (issueContext false branch)
- Add undefined prBody prompt test (null-coalescing branch)
Branch coverage: 83.53% → 85.06% (279/328)

* fix(#108): resolve all SonarCloud quality gate failures

Cognitive complexity reductions (max 5 per function):
- smoke-test: extract processPR helper (complexity 11→1)
- run-pipeline: extract processSinglePR + errorMessage (12→3, 8→3)
- open-review-pr: extract parseDraft, findValidDrafts, collectValidPlans,
  buildDryRunResults, promoteDomain, executeApply (26→1)
- filter: extract runLlmTriage + use Set for labels (7→2, 6→3)
- scraper: extract fetchMetadata, fetchDiff, fetchReviews (13→2)
- validate-schema: extract processEntry + logFileResult (8→4, 9→2)

Code quality warnings:
- Replace child_process with node:child_process in 3 files
- Use import matter from 'gray-matter' (remove unnecessary require assertions)
- Use String#endsWith for bot check, RegExp#exec for match
- Number.parseInt over parseInt throughout
- String#replaceAll over regex replace (distiller, open-review-pr)
- Flip negated condition in run-pipeline IIFE
- String(fm.source_pr) to prevent [object Object] in template
- files.toSorted() over files.sort() in validate-schema
- Update scraper.spec.ts proxyquire key for node:child_process

Coverage: 83.53% → 85.41% (281/329 branches)

* fix(#108): resolve remaining 7 SonarCloud issues

- smoke-test: extract filterAndDistill helper (processPR complexity 6→3)
  + prefer node:os import
- filter: extract lockfile/translation booleans in checkSkipRules (7→5)
  + extract isBugWithLinkedIssueAndMultiService/isFeatureWithLinkedIssue/
  isSharedLibsWithMultiService helpers for checkDistillRules (7→3)
- open-review-pr: pre-compute sourcePrStr as typed string to prevent
  [object Object] template literal stringification
- validate-schema: add localeCompare comparator to toSorted()

* fix(#108): extract checkSkipRules helpers to eliminate && complexity (7→5)

* fix(#108): resolve S5852 ReDoS in LOCKFILE_PATTERN regex

Replace unbounded .* with [^/]* in the lockfile alternation so the
wildcard cannot overlap the (?:^|/) anchor. Eliminates super-linear
backtracking (50k-char non-match: ~0.07ms) while preserving match
behavior across root-level and nested *.lock paths.

* fix(#108): use mkdtempSync for smoke-test temp paths

Resolves SonarCloud hotspots flagging predictable temp paths in a
publicly-writable directory. mkdtempSync creates a per-run dir with a
random name and 0700 permissions.

* feat(#108): reconcile frontmatter schema with #87 (camelCase)

Adopts #87's converged camelCase schema.json and schema-utils.ts verbatim
so the two PRs share one frontmatter contract and the 30 committed
agent-memory files validate natively (no lastUpdated->last_updated alias).

Distiller changes:
- emit camelCase frontmatter (lastUpdated, issueNumber, issueUrl) plus the
  now-required services and techStack fields
- derive issueNumber/issueUrl/id from the first linked issue, falling back
  to the PR number when the PR closes no issue
- serialize frontmatter with js-yaml (correct quoting; fixes unquoted title)
  and AJV-validate every draft before writing — malformed drafts route to
  _skipped.ndjson as flag-for-human instead of landing in _pending/

Addresses the review's blocking schema-reconciliation item.

* chore(#108): reset _skipped.ndjson to empty (drop dev-run log noise)

* docs(#108): document OpenRouter provider-agnostic rationale

* fix(#108): address review nits in pipeline stages

- filter: narrow LOCKFILE_PATTERN to known lockfile names (no longer
  skips unrelated *.lock files); pattern is now fully literal (no ReDoS surface)
- filter: drop redundant inner try/catch in llmTriage; runLlmTriage is the
  single error boundary (covers default + injected triageFn paths)
- scraper: drop unresolvable linked issues instead of returning empty stubs,
  so a 404'd reference can't flip a filter skip into a distill
- scraper: wrap metadata/reviews JSON.parse in ScraperError (carries prNumber)
  instead of leaking a raw SyntaxError
- distiller: slugify falls back to 'untitled' for symbol-only/non-Latin titles
- open-review-pr: escape Markdown special chars in draft titles in the PR body
- tests: relabel mislabeled single-service filter test; add coverage for all
  of the above (359 passing)

* fix(#108): address secondary review follow-ups 1-3

- scraper: fetch reviews with `--paginate --slurp` and flatten the
  array-of-pages instead of string-stitching `[...][...]` — no longer
  corrupts review bodies containing `] [` or breaks on empty pages
- open-review-pr: isolate promotion per domain — a domain failure is
  recorded as a 'failed' ReviewPRResult and the rest still run; clean up
  the orphan remote branch when PR creation fails after push
- filter tests: stub both LLM providers (ChatOpenAI + ChatAnthropic) and
  clear both API keys per-test, removing cross-spec env leakage; add an
  explicit OpenRouter primary-path test

363 passing, 86% branch coverage

* fix(#108): address secondary review follow-ups 4-7

- run-pipeline: validate --since/--pr as positive integers (error instead of
  silently processing nothing); guard the CLI IIFE behind require.main so the
  module is importable, and cover its orchestration functions (now 100%).
- scraper: handle reviews from deleted GitHub accounts (user: null) by falling
  back to the 'ghost' author instead of dereferencing null.
- open-review-pr: resolve source_pr 'owner/repo#N' refs to working /pull/N URLs
  via a new sourcePrUrl helper, instead of a dead repo-home anchor.
- package.json: add the smoke-test npm script.

Tests: 380 passing; coverage 97.21% stmts / 86.65% branch (>= 95% gate).

* fix(#108): reject partially-numeric --since/--pr (roborev MEDIUM)

Number.parseInt silently truncated '123abc' -> 123 and '1.5' -> 1, so
partially-numeric flag values passed the positive-integer check. Validate the
raw string with /^[1-9]\d*$/ before parsing, via a parsePositiveIntArg helper.
Adds regression tests for trailing characters and decimals.

* fix(#108): reject oversized --since/--pr values (roborev LOW)

A digit string large enough to overflow Number.parseInt passed the regex but
produced a non-safe-integer (and an invalid lookback date downstream). Parse
with Number() and require Number.isSafeInteger. Adds a regression test.

* refactor(#108): clear SonarCloud findings on PR #109 new code

- run-pipeline: use Number.NaN over NaN (S7773); throw TypeError for the
  argument-validation guard (S7786).
- open-review-pr: use String.raw for the Markdown-escape replacement (S7780);
  RegExp.exec() over String#match in sourcePrUrl (S6594); reduce cognitive
  complexity of promoteDomain and executeApply (S3776) by extracting
  stageDrafts, deleteRemoteBranch, and promoteDomainSafely helpers.

Behavior unchanged; 384 passing, coverage 97.2% (>= 95% gate).

* fix(#108): restore missing JSDoc opening tag in constants/index.ts

A `/**` line was dropped (likely during rebase), leaving the comment
body as bare tokens that TypeScript parsed as code — causing 30+ TS
errors and the "Build, lint, and test" CI failure.
@Hareet Hareet reopened this Jun 24, 2026
@sugat009

sugat009 commented Jun 26, 2026

Copy link
Copy Markdown
Member

Heads up, this one currently has merge conflicts with its base and will need a rebase before it can merge.

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Content review. Prose fidelity is high (8772 reproduces the real 283s->63s benchmark exactly). Blockers:

  • issue (blocking): identity keys record the PR number on 15 of 35 drafts (verified); 8838 closes no tracked issue.
  • issue (blocking): hallucinated identity on 9232. id/issueNumber/issueUrl = 137, but 137 is medic/care-teams#137 (a cross-repo link in the PR body) mis-stamped as cht-core; cht-core#137 is the unrelated "Restyle data records". PR #9232 actually closes #9231.
  • issue (blocking): 3 duplicate clusters (8 files): issue 9231 = [9232, 9282, 9317], 9431 = [9486, 9549], 9552 = [9553, 9555, 9569, 9570].
  • issue: related_issues: [] empty on every draft; domainFit: strong on every draft. Forced picks: 10390/10432 (cht-datasource data-layer, tied to tasks only via the target docs), 8932 (touches tasks.component.ts and contacts.component.ts equally), 10786 (telemetry + replications.js/purger.js pipeline).
  • nitpick: process-narrative leaks into prose on ~11-18 of 35 drafts (named reviewers, "CI green, all 47 checks pass", "the human reverted an AI change").

Clean bill: no secrets, no PII, schema 100% valid.

Hareet and others added 3 commits July 16, 2026 18:13
…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>
…ets)

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>
@Hareet
Hareet force-pushed the memory/promote-tasks-and-targets branch from 80e45db to 1818c23 Compare July 17, 2026 04:52
Hareet added a commit that referenced this pull request Jul 17, 2026
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>
Hareet added a commit that referenced this pull request Jul 17, 2026
…ion)

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

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>
@Hareet
Hareet requested a review from sugat009 July 17, 2026 04:59

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Content re-review (post-rework). Identity/dedup clean (incl. the 9232→issue 9231 cross-repo fix). Remaining:

  • issue (accuracy): 9553 invents a backport version (inline).
  • issue (domain): 10390 (target-intervals in cht-datasource) is library extension → data-access (inline). 10432 was relocated to contacts; same treatment applies.
  • nitpick (leakage): 10324, 9650 Domain Rationale scaffolding; strip.

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

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

Hareet and others added 7 commits July 28, 2026 10:44
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>
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>
…ks-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>
…argets)

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>
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>
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>
…nd-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>
Hareet and others added 5 commits July 31, 2026 23:30
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>
…-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>
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>
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>
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>
Hareet and others added 6 commits August 1, 2026 21:03
…argets)

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>
- 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>
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>
- 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>
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>
…ymbols

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

Hareet commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Both round-2 items are fixed, and putting this branch through the same verification the configuration and messaging batches got turned up a class none of us had seen — one epic reshaped five of these drafts before it landed. Details below, worst first.

Your two items

9553's invented backport version — fixed, and every number in the replacement checks out:

git -C $CORE log -1 --format='%h %s' c8a7f13
# c8a7f13 fix(#9552): handle rules-engine stale state after upgrade  for 4.13.x (#9555)
git -C $CORE branch -r --contains c8a7f13 | grep -E 'origin/[0-9.]+x$'   # origin/4.13.x only
curl -s https://api.github.com/repos/medic/cht-core/pulls/9555 | jq -r '.base.ref'   # 4.13.x

The draft now also records why there is no 4.1.x backport: that line's tip is 62aadbd (4.1.2, June 2023), which predates the October 2024 fix.

10324 and 9650 Domain Rationale scaffolding — stripped.

The thing worth your attention: one epic, five drafts

10324, 10371, 10390, 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, so their file paths are accurate for their own PRs and wrong as directions to anyone reading master:

the draft names master has
tests/e2e/default/analytics/analytics.wdio-spec.js tests/e2e/default/targets/analytics.wdio-spec.js — the directory was renamed
webapp/src/ts/libs/config.ts (created by 10507, renamed by 10436) nothing; only .mocharc.js survived, and the subtitle logic moved into rules-engine.service.ts
10390's ten target-interval.* files (of the 21 it touched) target.* — same feature, renamed wholesale
git -C $CORE ls-tree --name-only -r origin/master shared-libs/cht-datasource/src | grep target
#   .../src/local/target.ts  .../src/remote/target.ts  .../src/target.ts     ← no -interval
git -C $CORE log --all --diff-filter=A -- '*target-interval*' | wc -l        # 0 — never on any branch

The four that stay here are now time-scoped with their landed locations. 10390 is not — its vocabulary needs a rewrite, not a patch, and I would rather flag that than half-fix it. It does now carry the same "paths are as of this PR" banner as the others, so nobody reads those ten names as directions to master in the meantime. See below.

Everything else the pass found

Twelve self-contradictions across nine drafts, the same class you identified on #120: the grounding pass corrected the sections that assert mechanism against code and left summary and Design Choices asserting what it had just disproved. Each verified against cht-core before rewriting — 9232 claimed the filter shows "only for multi-facility users" while its own Solution says there is no facility-count gate; 9705's Design Choices claimed the fix throws on write errors when it changed a read catch-all and the file's one bulkDocs still only console.errors; 10623 said filters were reused when overdue-filter and task-type-filter are new components on master.

Then five more that my own first round of fixes created — correcting one section and leaving a sibling saying the old thing, which is the failure mode this whole exercise is about, committed by the person fixing it. 9232 and 9553 each needed three passes before every section agreed.

Then eighteen more across thirteen drafts in a final round of repeated passes, each checked against cht-core rather than reasoned about. Worth naming, because two were mine and several are classes I had not been testing for:

  • 10480's summary said the PR generalised the unread-count flow to carry either count. It does not: setBubbleCounter still only carries reports and messages, and getBubbleCounter spreads that and adds task: taskState.overdue?.length || 0 from a different slice. What was reused is the bubble at the display layer, not the dispatch. My own Code Patterns rewrite had said so plainly and I left the summary contradicting it.
  • 10480 again: I had asserted "a task due later today still counts" without checking why. It is true, but only because moment(emission.dueDate, 'YYYY-MM-DD') parses date-only, putting a task due today at midnight — already before moment(). The draft now states the mechanism and cites Show a count of overdue tasks in the nav bar cht-core#3943, which asks for exactly "Overdue" and due-"Today" and "not things that are due tomorrow".
  • 10436's Testing called two page objects new and both e2e helpers new. git diff-tree on its own PR says both page objects were modified, targets-helper-functions.js was added and aggregates-helper-functions.js was modified. This one ground-claims caught; earlier passes had simply never sampled that sentence.
  • 10507 described libs/config.ts as "a helper this PR introduced — was updated to", both at once, and gave it a present-tense home in Code Patterns while the same draft says it is not on master.
  • 10436 referred to libs/config.ts twice more without that scoping, and 10371's Code Patterns implied one component records both open and selection telemetry — analytics-filter.component.ts records only the :open keys; the selection keys live in the sidebar filter component.
  • 10623's Code Patterns named reports and messages as the components that stopped duplicating lineage filtering. tasks.component.ts dropped its ExtractLineageService.removeUserFacility call too, which is the whole reason the refactor appears in a tasks PR.
  • 9232's summary said multi-facility users "had no way to scope the aggregate targets analytics view by place", while its own Problem section says they were blocked from viewing aggregate targets at all. Allow users to view aggregate targets and filter by facility_id and reporting period cht-core#9231 is explicit — "in 4.9.0, users assigned to multiple facility_ids cannot view aggregate targets" — so the summary was understating a regression as a missing feature. Third time this draft's summary needed correcting.
  • 10362 called the comparator order-by-due-date-and-priority in two sections. That is the name of its test file; the export is orderByDueDateAndPriority. The draft had both, and used the filename where the symbol belonged.
  • 11142 attributed FreetextFilterComponent to the tasks module. It lives in components/filters/freetext-filter/freetext-filter.component.ts — the Tasks page reuses it, which is the actual point, so the draft now says which file it is.
  • 10423's Testing listed eight test files under one "Added". Six really were added; the two e2e WDIO specs (analytics.wdio-spec.js, target-aggregates.wdio-spec.js) already existed and were modified. Both ground-claims passes caught this one, which is what recurrence is supposed to look like.
  • 10324's Testing opened "Added an e2e WebdriverIO spec … and Karma unit specs …". Its PR adds no test file at all: every spec listed was modified, and the sidebar-filter spec was renamed. The section now says so outright.
  • 9553's summary blamed "a CHT upgrade that changes target configuration". The Root Cause blames the #9486 persisted-shape change, and the code agrees — isStale is (state) => !state || !state.targets || !state.aggregate, a pure shape check that never reads configuration. The trigger is upgrading across #9486, full stop.
  • 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. Same class as everything else on this branch, found on the fourth round of passes.
  • 9232 wrote "added new translation keys (api/resources/translations/messages-en.properties)". The keys are new; the file is not.
  • 9553's 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: the function returns early when stateCalculatedAt falls inside the current interval, and otherwise aggregates against calendarInterval.getInterval(monthStartDate, stateCalculatedAt) — the stale interval — and stores that. Reading against the active interval is the one thing it never does. And true to form, my correction introduced the next finding: it named calendarInterval.getInterval and stateCalculatedAt — both real at the anchor, both gone from master because #9718 (in this batch) removed the whole mechanism. The drift check caught that in three of the next four passes; the sentence is now time-scoped and points at the 9718 draft.
  • 8772's summary described the short-circuit as firing "when the key count reaches 500 (the guard bypasses only length < MAX_QUERY_KEYS)". That is correct and almost unreadable — the parenthetical states the inverse condition, and two independent passes read it as contradicting the Solution. The guard is !params?.keys || params.keys.length < MAX_QUERY_KEYS; the summary now says so in one direction only.

I also swept all three domains mechanically for any path a draft lists that is absent from origin/master and then refers to elsewhere without scoping. Fifteen hits: ten are past-tense statements about what the PR did and are correct as written (10279 removing emitter.nools.js is the point of that PR), and five are present-tense claims in 3406, 4039 and 4374 — all three of them anchorless drafts outside #120's diff, already carved out. Nothing in scope on any of the three branches.

All 25 timestamps were stale — the relink, dedup and grounding commits never bumped them.

10390 is left for the data-access work

Your inline comment asks to move it to data-access with secondaryDomains: [tasks-and-targets]. I have left it here — whether it ends up moved or simply carries a secondary domain is exactly the sort of thing that taxonomy change should decide — on the strength of your own recommendation on #122:

Ship data-access + secondaryDomains as one coordinated schema/taxonomy PR (alongside the agreed data-access work, not through these content chores) … Not blocking any single PR.

Doing it here would mean three open PRs racing to add the same enum value, and a corpus where one extender moved while the other seven did not. There is now a second reason: 10390's entire file and API vocabulary needs rewriting to the landed target.* names, so it wants a rewrite by someone thinking about cht-datasource, not a re-key bolted onto a tasks PR. A proposal for that work is being written up separately, and an early count suggests the datasource scatter is considerably wider than the drafts named on these three PRs. If you would rather it move now, say so and I will do it — but I think the sequencing you wrote is the right one.

Gate

validate-schema 64/0. verify-drafts --online (identity, duplicates, near-miss symbols, leakage, cross-references, timestamps): 0 blocking, 0 warnings, 0 unverified across all 25. ground-claims: 0 ungrounded, 0 unverifiable, 0 anchor-unusable, 0 unmatched code fences, ~390 grounded per pass, all 25 anchored — the five epic children via #10423's squash 622c625427, the other twenty at their own commits. check-coherence: 0 contradictions.

Those are not single-pass numbers. On this exact content the gate ran four ground-claims passes and three coherence passes, all clean, after a sequence in which almost every pass had turned up something the one before it missed. That is the point of the count, not decoration.

Two stale-as-written items remain, and they are disclosed rather than fixed — 9232 names analytics-target-aggregates-sidebar-filter.component.html and its spec, true at its anchor and renamed to analytics-sidebar-filter.* when the 10140 epic generalised the sidebar past target-aggregates. They are what stale-as-written exists to record: accurate when written, moved since — and the draft's own epic banner already tells the reader its paths are PR-era.

I also re-ran configuration and messaging under the corrected tooling to be sure none of the five tool fixes weakened an earlier verdict: configuration 99 grounded / 0 ungrounded, messaging 225 grounded / 0 ungrounded (its 27 unverifiable are the five anchorless drafts carved out of #120, unchanged). One of the fixes actually removed a false finding that had appeared on #120's branch — 10230's nepal-doit-sms, where extraction asked whether nepal-doit-sms.js contains its own filename.

Four caveats I would rather state than have you find. The first is the one that matters.

  • Both checkers sample rather than exhaust, and I can now put a number on it. Two ground-claims passes over byte-identical drafts each surfaced one ungrounded claim — and it was a different claim each time (11142's FreetextFilterComponent, then 10362's comparator name). Either pass alone would have reported "1 finding, fixed, done" and shipped the other. Coherence behaves the same way: 9232's and 9553's contradictions appeared in every pass, while 9022's surfaced in one of three. So the number below is from repeated passes, and a single clean pass is weak evidence — including mine.
  • Five sources of that noise turned out to be tool bugs, and are fixed on the tooling branch. check-coherence was counting pairs whose own rationale read "These do not conflict". ground-claims reported a Root Cause claim as ungrounded when extraction had pinned it to a file the PR created — a pairing no commit can satisfy. It resolved a bare filename onto its real path but not a path tail, so 11142's true "reducers/global.ts only gains a search?: string field" came back as a misattribution. It accepted a multi-word noun phrase ("interval turnover") as a symbol. And it asked whether nepal-doit-sms.js contains its own filename. All five now resolve or discard rather than report, with tests. Each only ever turned a false alarm into silence, so no earlier verdict on any branch is weakened by them — but they do not make either checker exhaustive.
  • The coherence gate proves the quotes are real, not that they contradict. That judgement is still a model's, and on this branch two of its findings (10371, 8772) turned out to be correct-but-unreadable prose rather than contradictions. I rewrote the prose anyway — if a checker misreads a sentence twice, a person will too.
  • Claim extraction is exhaustive only over code-shaped claims, and one useful class sits outside it. Every repo path and backticked identifier is enumerated deterministically, so that tier is reproducible run to run; semantic claims ("the fix prevents duplicate sends") are still sampled and cannot be enumerated. Specifically: added-versus-modified is checked only when the model happens to sample the sentence — five of the findings above are that class. Making it deterministic means scoping "Added X, Y and Z" across a coordinated list, which I tried and abandoned at 126 false positives out of 164, so I raised the pass count instead of shipping a noisy check. Measured on 13 unchanged drafts, two extraction passes shared only 29% of their claims — which is why the mechanical tier was worth making deterministic, and why I am not claiming the semantic one is complete.

@Hareet
Hareet requested a review from sugat009 August 3, 2026 01:13

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 4. The rework resolved the round-3 self-contradiction class and CI is green, so most of this branch is in good shape: 10279 and 10337 came back fully clean, the 9553 fabricated 4.1.x backport is gone, and leakage is stripped from 10324 and 9650.

Requesting changes on five items I verified myself against the PRs' own diffs and cht-core master. Four are blocking-ish and one is a label error an agent would grep for.

On triage, so this is not a wall of noise: I deliberately dropped about six items that were pure archaeology. Claims like which PR first added a symbol, or what a signature looked like at an old commit, are not worth fixing when the draft states today's reality correctly, and recording the old shape would actively mislead an agent trying to call it. Where a draft states a true-on-master fact with a historical verb ("gains X", "at this commit"), the only real fix is the verb, and I have not filed those. What I have filed is limited to: false on current master, semantically inverted, or fabricated.

One pattern worth naming. The two 9553 paragraphs are byte-identical to the pre-rework version, so 19 commits rewrote everything around them and never touched them. The 10436 and 10362 items are the mirror image of round 3: the pass verified that symbols exist but not that the attribution was right. A sweep of every "this PR added X" and "on master Y" sentence against gh pr view --json files and git grep origin/master would close the whole class.

Hareet and others added 3 commits August 7, 2026 11:43
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>
…ded 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>
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>
@Hareet

Hareet commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Round 5 re-review request:

All five items are fixed. Each was re-derived from the PRs' own diffs and cht-core
master before editing rather than taken on your word, and all five checked out.

Then I did the sweep you asked for in the review body — "every 'this PR added X'
and 'on master Y' sentence against gh pr view --json files and git grep origin/master"
— mechanically, over all 25 drafts. It found one more instance of
exactly your class, and the gate found five self-contradictions on drafts you did
not name. Details worst-first.

Your five

10436, the mocha harness (blocking). You were right that the PR removed all of
it; the draft said it added it. From its own file list: config.spec.ts +0/-103,
tsconfig.mocha.json +0/-9, .mocharc.js +0/-2, libs/config.ts +0/-26 — all
deletions. Also fixed the two claims in the same sentence: webapp/tsconfig.spec.json
is the webapp-root karma tsconfig, is on master, and only gained sinon-chai types
here (+2/-1); getValueFromFunction was deleted outright, not folded.

The replacement says where subtitle derivation actually lives, which is your
suggestion on line 64: this PR added getReportingMonth(settings, reportingPeriod)
to rules-engine.service.ts (master :588), surfaced per target as reportingMonth
and rendered via subtitle_translation_key.

9553, the symptom and mechanism (blocking ×2). Both were inverted, and both
were byte-identical to the pre-rework version exactly as you noted. #9552 is a
crash, not silently wrong numbers:

git -C $CORE show 'dc47c51^:shared-libs/rules-engine/src/target-state.js' | grep -n 'Object.keys(state.targets)'
#   43, 67, 227 — a pre-#9486 bare map has no .targets, so Function.keys throws
git -C $CORE show fe795fb --stat | tail -6
#   +1 rules-state-store.js, +13/-6 target-state.js, rest tests — no interval logic
git -C $CORE show 'fe795fb^:shared-libs/rules-engine/src/provider-wireup.js' | sed -n '313,332p'
#   handleIntervalTurnover, isBetween(currentInterval...), getInterval(...) all already there

So the turnover facet is the same shape bug: handleIntervalTurnover read the
persisted blob to write the previous interval's target doc and hit the same throw,
and the base fix's discard-and-rebuild would have dropped those emissions first.
Title, summary, Problem, Root Cause, Solution, Testing and Related Issues now all
tell that story, and the e2e case is named — "should handle old format of the
rules-state-store"
— instead of being described as a configuration change. Your
line-99 note is in too: what #9569/#9570 added coverage for is the shape check,
migrateStaleState, and commitTargetDoc receiving the previous interval's
aggregate. (One extra: Code Patterns credited the removal to #9714, which is the
issue; the PR is #9718.)

10362, the comparator (blocking). Confirmed on master — task-utils.js exports
only setTaskState, the comparator is reducers/tasks.ts:15, and the service
imports it from @mm-reducers/tasks at task-notifications.service.ts:11. The
draft now states master's location and keeps the round trip (reducer → task-utils
by this PR → reducer by #10701) as explicit history, with #10701's own reason:
task-utils is for the SMS tasks attached to reports, not rules-engine tasks.

10324, the radio labels (blocking). Confirmed: both filters use
targets.this_month.subtitle / targets.last_month.subtitle = "This month" /
"Last month" (analytics-sidebar-filter.component.ts:42-45), and "Previous month"
is zero-hit in messages-en.properties. Summary and Solution now say "Last month"
and note that ReportingPeriod.PREVIOUS is an enum value, not a label.

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

The sweep, and what it caught

Your closing note was the useful part of the review, so I mechanised both halves
and ran them over all 25 drafts.

Path half (git grep origin/master): every path a draft names that is absent
from master must sit on a line that scopes it. 49 hits, all accounted for —
relative fragments that do resolve, banner-scoped epic paths, the deliberately
deferred 10390, and 9232's two disclosed stale-as-written entries. Nothing new.

Attribution half (gh pr view --json files, via the API): every add/remove/modify
claim checked against the PR's real file status, and every "this PR added symbol"
claim checked against the PR's own added lines. To show the screen has teeth rather
than just returning green, I ran it against the pre-fix text and it reproduces
your blocking finding unaided:

10436:43  [webapp/src/ts/libs/config.ts]   prose says ADDED/new, PR says REMOVED
10436:43  [tsconfig.mocha.json]            prose says ADDED/new, PR says REMOVED
10436:43  [config.spec.ts]                 prose says ADDED/new, PR says REMOVED

On the fixed corpus the file-level screen returns 64 candidates and the symbol-level
screen 12, and every one is the "added a symbol to an existing file" shape —
"Added a dbQuery wrapper in pouchdb-provider.js" — or a removal correctly
attributed to a different PR. I adjudicated all 76 by hand.

It did find one real instance of your class, on a draft you had already reviewed:

10324 credited itself with the sidebar's telemetryKey input. It never touches
it. git log-free check, straight from the two PRs' diffs:

# 10324's additions to the component: userFacilities, showFacilityFilter — no telemetryKey
# 10371's additions: @Input() telemetryKey: string = 'target_aggregates';

Fixed, and it now points at #10371.

Worth flagging one thing the sweep surfaced that I deliberately did not
enshrine, because it is the archaeology you said not to file. #10371's diff binds
targetModuleId = "targets" in analytics-targets.component.html while the
@Input it adds in the same PR is telemetryKey — the attribute matched nothing
at that commit and was corrected to telemetryKey later on the feature branch, so
no commit on master ever contains targetModuleId. Recording the dead name would
mislead a reader; the draft states the landed form and stops there.

Five more the gate found on drafts you did not name

9486 took three passes because my first two fixes were each wrong in a new way
— the failure mode this whole exercise is about, committed twice by the person
fixing it. The summary said targets were computed "only ... when visiting specific
pages" while its own Root Cause described two 120s ensure-freshness debounces
(ENSURE_FRESHNESS_SECS = 120 at dc3ef42ab^, used at :133 and :148). Calling
that "a 120s debounce" then contradicted "two separate"; fixing that left Problem
asserting nothing ran on incoming changes while Root Cause said the hook called
updateEmissionsFor per change. At dc3ef42ab^ that function body is
.then(contactIds => rulesStateStore.markDirty(contactIds)) — it invalidates, it
does not recompute. Problem now says precisely that.

9705 — the Problem opened by putting the swallowed error on the write while
the summary, its own next sentence, Root Cause, Solution and Design Choices all put
it on the read that precedes the write. The read is right.

10390 — Domain Rationale said "all changed files are shared-libs/cht-datasource
plus one API controller"; the PR also changes api/src/routing.js, adds integration
tests under top-level tests/integration/, and touches .mocharc.js and
package.json. (Caught twice: once as filed, once after my own first correction
still under-counted.)

8932 — the title called the flashed strings "empty states" while Root Cause
shows both render only when the has-items flag is true (hasTasks,
hasContacts), which makes them end-of-list messages — the opposite. Title and two
tags follow.

10371 — after the segment fix, the Solution read as though everything new was
on the targets page while summary and Domain Rationale said target aggregates. The
input defaults to target_aggregates, so the aggregates page gets selection
telemetry with no wiring and the targets page overrides the key; all three sections
now say that.

10390 still stays put

Unchanged from last round and for the same reasons: the taxonomy change is your own
sequencing recommendation on #122, and 10390 additionally needs its whole
target-interval.* vocabulary rewritten to the landed target.* names — a rewrite
by someone thinking about cht-datasource, not a re-key bolted onto a tasks PR. The
epic banner keeps anyone from reading those ten names as directions to master.

Gate

validate-schema 89/0/3-skipped. verify-drafts --online 0 blocking, 0 warnings,
0 unverified
across all 25. ground-claims 0 ungrounded, 0 unverifiable, 0
anchor-unusable, 0 unmatched code fences
, all 25 anchored — the five epic children
via #10423's squash, the rest at their own commits. check-coherence 0
contradictions.

The sequence matters more than the totals. Across the round the gate ran 14
ground-claims passes and 15 coherence passes
. The first of each turned up 4
ungrounded claims and 3 contradictions, and almost every pass after that found
something the one before it had missed — three separate rounds on 9486, two on
10390, and one on a draft I had already "finished" twice. It went quiet only at the
end, on frozen bytes, and the last five passes were run against the exact committed
content.

Two gate findings were the gate misreading itself, and they are worth naming
because anyone re-running this will hit them.

The first changed the prose anyway. A pass flagged
sidebar_filter:analytics:<telemetryKey>:reporting-period:select as a symbol that
does not exist — correctly, since it is a placeholder form no literal git grep
can match. It only existed because I had written a concrete-looking key with a
placeholder inside it. 10371 now quotes the real template literal, which is
byte-identical at the anchor and on master
(analytics-sidebar-filter.component.ts:125 both places).

The second I left alone: one pass in fourteen flagged target-interval as
non-existent in 10423, from the sentence "the document type was kept as 'target'
(its actual type) rather than 'target-interval'."
The symbol's absence is the
thing the sentence asserts, so the probe is confirming the claim while reporting it
as ungrounded — ground-claims has no notion of a counterfactual. I would rather
keep the rejected name on the record, since 10390's file vocabulary is still built
entirely out of it, than reword a true sentence to satisfy a probe.

Two stale-as-written items remain and are disclosed rather than fixed: 9232's
analytics-target-aggregates-sidebar-filter.component.html and its spec, true at
its anchor and renamed when the 10140 epic generalised the sidebar. That is what
stale-as-written is for, and the draft's epic banner already tells the reader its
paths are PR-era. A third drift item, 8932's setTasksList(hydratedTasks), I did
fix rather than disclose — the local is gone from master, so the name was purely
greppable-and-wrong with nothing gained by keeping it.

@Hareet
Hareet requested a review from sugat009 August 10, 2026 04:20

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

Approving. All five items from review 4845979803 are fixed, and I re-verified each against the PRs' own diffs and cht-core master rather than taking the commit message's word for it.

  • 9553 now tells the crash story throughout (title, summary, Problem, Root Cause, Related Issues): TypeError: Cannot convert undefined or null to object at Object.keys in aggregateStoredTargetEmissions, with the 4.12 to 4.13.x report. The turnover paragraph correctly states that detection pre-existed (handleIntervalTurnover's isBetween and calendarInterval.getInterval were both present before #9569) and that #9569 added no interval logic, and it adds that #9718 has since removed the mechanism entirely.
  • 10436 states the deletions with exact counts and puts webapp/tsconfig.spec.json correctly on master.
  • 10362, 10324, 10371 all corrected; 10324 now documents explicitly that "Previous month" is only the ReportingPeriod.PREVIOUS enum value and never a UI string, which is the distinction that made the original wrong.

Two of these went past what I filed, which I want to credit specifically. On 10436 I could only establish that getValueFromFunction was zero-hit on master; you found the actual successor, and it checks out: getReportingMonth is at rules-engine.service.ts:588 on master and PR 10436's own diff adds it. On 10362 you recorded the full round trip (added to the reducer by #9980, moved to task-utils here, reversed by #10701) rather than just correcting master's location.

The self-found fixes in 58a6ca9 and 4ec1d12 are real precision gains rather than churn, and I found no new error introduced by them. The standout is 9486: the old summary said targets were only computed on page visits, and the correction that there were already two once-per-session 120s ensure-freshness debounces verifies exactly at the anchor's parent. ENSURE_FRESHNESS_SECS = 120, one debounce calling fetchTaskDocsForAllContacts() and one calling fetchTargets(), each armed once inside the isEnabled block and never re-armed. That is a claim I never flagged, caught by your own gate.

Also worth noting the method: re-deriving each item from primary sources instead of trusting the review is the right instinct, and it is what caught my own overreach earlier in this batch.

Pre-merge checks: CI green on all four at 58a6ca9, diff confined to agent-memory/, and schema.json is byte-identical to main now that #120 and #130 have merged, so it carries no duplicate change.

One open thread I am deliberately not blocking on. The 10390 domain question (cht-datasource work belonging in data-access) was parked on 2026-07-21 pending the domain existing. #152 now adds data-access and secondaryDomains, so that is answerable whenever you want to sequence it, and this PR's reword of 10390's rationale (naming the controller, the routing.js registration and the test wiring) makes the file set read as even more clearly library-centric. Happy for that to be a follow-up rather than a change here.

Resolving my stale threads on this PR since the underlying items are all addressed.

@Hareet
Hareet merged commit 041f132 into main Aug 11, 2026
4 checks passed
@Hareet
Hareet deleted the memory/promote-tasks-and-targets branch August 11, 2026 20:07
Hareet added a commit that referenced this pull request Aug 12, 2026
… 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>
Hareet added a commit that referenced this pull request Aug 12, 2026
…ontacts)

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hareet added a commit that referenced this pull request Aug 12, 2026
…ntacts)

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hareet added a commit that referenced this pull request Aug 12, 2026
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>
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