Skip to content

chore(memory): promote strong-fit forms-and-reports drafts from memory-pipeline for review - #122

Open
Hareet wants to merge 23 commits into
mainfrom
memory/promote-forms-and-reports
Open

chore(memory): promote strong-fit forms-and-reports drafts from memory-pipeline for review#122
Hareet wants to merge 23 commits into
mainfrom
memory/promote-forms-and-reports

Conversation

@Hareet

@Hareet Hareet commented Jun 24, 2026

Copy link
Copy Markdown
Member

Promotes 47 strong-fit forms-and-reports drafts from agent-memory/_pending/ into agent-memory/domains/forms-and-reports/issues/ for squad content review.

Categories: feature (21), bug (20), improvement (6)
Themes: Enketo widgets & XPath extensions, cht-datasource report create/update, SMS parser, form validation/submission hardening.

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

@Hareet

Hareet commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

Needs to be rebased with main after #119 is merged

@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 across a wide sample (10022, 10246, 10730, 8746 all verified faithful). Blockers:

  • issue (blocking): identity keys record the PR number on 25 of 47 drafts (verified); 8740 closes no tracked issue.
  • issue (blocking): 6 duplicate clusters (13 files): issue 8745 = [8746, 8748, 8752], 9429 = [9434, 9436], 9604 = [9608, 9610], 9835 = [10022, 10246], 10040 = [10071, 10099], 10041 = [10180, 10200]. Plus near-duplicate content: 10922 (#10904) and 11116 (#10700) distill the same attachment-routing feature.
  • issue: related_issues: [] empty on every draft; domainFit: strong on every draft. Forced picks: 9512 (route guard across seven *.routes.ts, no form-engine code), 9513 (once-a-day display gating), 11023 (geolocation.service.ts only), 9641 (API startup resilience). Borderline: 9592 (training-materials page that does include training forms).
  • nitpick: classifier/seed reasoning leaks into ## Domain Rationale of the 8745 cluster ("seed-3 principle").
  • nitpick: category vs slug/PR type: 9414 is bug but slug feat9413; 9840 is feature but the PR is a fix (its key 9844 is correct).

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

category: bug
domain: forms-and-reports
domainFit: strong
issueNumber: 8748

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: 8746/8748/8752 are three PRs (base + two backports) for one issue #8745, distilled as three separate memories; they should collapse to one. The "## Domain Rationale" here also leaks the internal "seed-3 principle" phrasing.

---
id: cht-core-8119
category: improvement
domain: forms-and-reports

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (domain-fit): this is an Angular canDeactivate route guard wired across seven *.routes.ts (about/analytics/contacts/messages/reports/tasks); it is navigation plumbing, not forms-and-reports.

category: feature
domain: forms-and-reports
domainFit: strong
issueNumber: 11116

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: near-duplicate of 10922 (both route Enketo attachments to the owning sub-doc, same fixtures); 10922 closes child #10904, 11116 targets epic #10700.

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

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>
@Hareet
Hareet force-pushed the memory/promote-forms-and-reports branch from 573ca6b to 849aa01 Compare July 17, 2026 04:40
@Hareet
Hareet requested a review from sugat009 July 17, 2026 04:43
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
Companion to the forms (#122) seeder's cross-domain dedup: the curated
forms-and-reports memory for issue #10443 (default training forms
missing from Docker images) lives on main and now records PR #10445 in
its source_prs, so the duplicate draft here is removed.

validate-schema 107/107; no duplicate issueNumbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…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>
@sugat009

Copy link
Copy Markdown
Member

Proposal: a first-class data-access domain + a weighted secondaryDomains[] field

This is a cross-cutting taxonomy/schema item spanning #122, #123, #132 (and the merged #129). Not blocking any single PR — it is the decision the team already partly made in #86.

The problem. cht-datasource work has no clean home in the 9 domains, so library-extension drafts are scattered: #122 (10064/10071/10180, plus the dropped 10022/10246), #123 (10390; 10432 relocated to contacts), #132 (10043 PersonQualifier, 10057 PlaceQualifier, 9266/9281 getPeople). This is the context-selection gap raised in #86 (the distiller injects the wrong patterns for these tickets).

Extend vs use (classification rule): a draft whose primary work extends the cht-datasource library API — its anchor PR touches only shared-libs/cht-datasource — gets data-access as primary domain; a draft that merely consumes cht-datasource keeps its product domain with subDomain: cht-datasource. E.g. 10071 (createReport) is 100% cht-datasource → extend; 9755 touches no cht-datasource files → use.

Already agreed. Hareet proposed a first-class data-access domain (organized on cht-core #11174's Contact.v1/Report.v1 / Person/Place hierarchy), and we settled it in the team meeting to add it, to be populated by re-running the #119 pipeline over the datasource tickets, sequenced after #127 Langfuse for before/after traces. No tracker for it surfaces yet — worth filing.

The secondary-domain piece. Some datasource work is about a product area (e.g. 10071 = data-access primary, forms-and-reports secondary). The schema cannot express that today (subDomain is a free-text sub-area; the code-encoded rule is "one primary domain + related_workflows"). Proposed: add a secondaryDomains: CHTDomain[] field — reuses the CHTDomain enum, array-valued, and carries "smaller power" in retrieval (the consumer, calculateSimilarityScore / research + code-gen, weights a primary-domain match fully and a secondary-domain match at a fraction). This recovers the cross-domain breadth singular domain loses (my #135 comment) without two co-equal primaries. Keep it distinct from related_workflows (a CHTWorkflow) and related_domains (the domain graph). Do not auto-emit it from the distiller initially — seed it human-set on the datasource cluster until primary-domain accuracy is solid.

Recommendation. Ship data-access + secondaryDomains as one coordinated schema/taxonomy PR (alongside the agreed data-access work, not through these content chores), then re-key the extenders (domain: data-access + secondaryDomains: [<product>]) and keep consumers in their product domain with subDomain: cht-datasource. NB the 10022/10246 report-datasource content already lives in #132's 9835 contacts draft — more evidence it wants a data-access home.

@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 are clean. Remaining:

  • issue (accuracy): 9227 names a non-existent XPath function (inline).
  • issue (domain): the createReport / report-update drafts (10064/10071/10180) extend the cht-datasource library and belong in the agreed data-access domain (inline on 10071).
  • nitpick (domainFit): 9592 is strong but is the same training/navigation class as its weak siblings 9512/9513 (inline).
  • nitpick (leakage): 11165 Domain Rationale scaffolding; strip.
  • (10922/11116: only a minor gap — the contacts-side child issue 10903 has no draft; listing 11116 in source_prs is defensible.)

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 a separate detailed comment on this PR.

@@ -6,17 +6,20 @@ subDomain: enketo
issueNumber: 9227

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (accuracy): this draft (Solution section, ~L30) names the XPath function cht:luhn-check, but the real function is cht:validate-luhn (registered in PR #9220 as 'cht:validate-luhn': luhn; current webapp/src/js/enketo/medic-xpath-extensions.js and the CHT docs use it, added 4.10.0). cht:luhn-check never existed — an agent grepping the corpus would find a non-existent function. Please rename throughout.

---
id: cht-core-10040
category: feature
domain: forms-and-reports

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (domain): this PR (and 10064/10180) touches only shared-libs/cht-datasource — createReport/report-update library API. Per the team-agreed data-access domain (#86), these library-extension drafts belong in data-access with secondaryDomains: [forms-and-reports], not primary forms-and-reports. See the detailed data-access proposal comment on this PR.

id: cht-core-9598
category: feature
domain: forms-and-reports
domainFit: strong

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 (domainFit): graded strong, but this is a training-materials/navigation page — the same class as 9512/9513, which are weak. Re-grade to weak for consistency.

Hareet and others added 5 commits August 9, 2026 22:22
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>
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>
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>
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>
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>
Hareet and others added 14 commits August 9, 2026 22:49
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>
  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>
  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>
… 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>
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>
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>
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>
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>
…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>
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>
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>
…_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>
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>
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>
@Hareet

Hareet commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Request for re-review

All four items are addressed — one of them by disagreeing with you, with reasons
below. Beyond them, running this branch through the same gate #123 got turned up
a long tail: 19 commits across the batch, most of it found by the gate rather
than by the review, and a good deal of it introduced by earlier fixes in this
same sequence.

Two of those commits are filename corrections rather than prose. 8740 was keyed
in its filename to 7462 ("Make code for Enketo forms reusable outside cht-core")
while its frontmatter, correctly, says 7599 ("Update to latest enketo-core") —
renamed to match. 8656 was renamed from …-make-xpath-date-extensions-consistent
to …-make-xpath-date-tests-timezone-independent, because the change is a test
determinism fix and the old name described a source change it does not make.

Your four

9227, the XPath function that never existed — fixed. The registration is
'cht:validate-luhn': luhn at medic-xpath-extensions.js:226; cht:luhn-check
is zero-hit in cht-core.

git -C $CORE grep -n 'validate-luhn' origin/master -- webapp/src/js/enketo/medic-xpath-extensions.js
#   226:    'cht:validate-luhn': luhn,

Worth flagging that this one is not confined to the PR: 9227 is one of ten
hand-authored drafts already on main, and the landed copy still says
cht:luhn-check twice. Merging this repairs the live corpus, so it is a slightly
higher-value merge than the diff suggests.

10064 / 10071 / 10180data-access — deferred, and now recorded in
the drafts rather than only in this thread. Same call as #123's 10390, on your
own recommendation:

Ship data-access + secondaryDomains as one coordinated schema/taxonomy PR
Not blocking any single PR.

Neither the enum value nor the secondaryDomains field exists in the schema yet,
so a re-key here would be a content PR inventing taxonomy. What was wrong is that
a reader of the corpus saw three cht-datasource extenders keyed forms-and-reports
with no explanation. Each now records in Domain Rationale that the re-key was
requested and deliberately not made here.

One correction while doing it: the premise does not hold uniformly. 10071 and
10180 are pure shared-libs/cht-datasource, but 10064 is not — its diff
also carries api/src/controllers/report.js, shared-libs/lineage/src/index.d.ts,
two webapp files and integration tests. I had written "entirely
shared-libs/cht-datasource" into its Domain Rationale on your description, and
three consecutive coherence passes caught it against the draft's own Solution
section. 10064's case for data-access is therefore weaker than its two
siblings', which seems worth knowing when the taxonomy PR gets written.

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

9592 domainFit — this is the one I'd push back on. It is not the same
class as 9512/9513. Those two are navigation and scheduling: 9512 is a
canDeactivate guard across *.routes.ts, and 9513 gates display frequency in
training-cards.service.ts, which references no form-engine code at all. 9592
adds training-cards-form.component.ts, which owns the Enketo render/save
lifecycle:

git -C $CORE grep -n 'renderForm\|saveForm\|formService\.' origin/master \
  -- webapp/src/ts/components/training-cards-form/training-cards-form.component.ts
#   124:  private async renderForm(formConfig: FormConfig) {
#   130:      this.form = await this.formService.render(formContext);
#   188:  async saveForm() {
git -C $CORE grep -c 'FormService' origin/master -- webapp/src/ts/services/training-cards.service.ts
#   (no match)

So the grading tracks a real distinction rather than an inconsistency: this PR
builds a form-rendering surface, its siblings do not. The Domain Rationale now
spells that out explicitly so the next reader does not have to re-derive it.
Happy to drop it to weak if you would rather the training-cards cluster be
uniform — say the word.

11165 Domain Rationale scaffolding — stripped.

Round-1 items, re-verified rather than assumed

  • 8746/8748/8752 collapse to a single draft keyed to issue 8745.
  • 9512 is weak.
  • 11116 has no standalone draft; it is folded into 10922 via source_prs,
    which is the treatment you called defensible.

What the gate found that the review did not

The bulk of the 19 commits. Grouped by class, since the individual items are in
the commit messages:

  • Fabricated or unresolvable symbols and paths10917 described the widget
    as renamed from hidden-field-list.js, a file added in no commit on any ref;
    the widget landed as hidden-group.js outright, and the narrower name existed
    only in review. (Worth being precise, since I got this wrong myself while
    checking it: HiddenFieldList is a real cht-core symbol — it lived in
    enketo-translation/enketo-form-manager and was removed by cccce201e. It
    has nothing to do with this widget, which is why the draft now speaks only of
    the name the widget itself carried in review.) Also: enketo-core module paths
    written as though they were repo paths, and a bare contact-photo.component.ts
    when the resolvable path is under webapp/src/ts/components/contact-photo/.
  • Claims true of one PR attributed to another across the epic-branch drafts,
    where #10071/#10099 and #10180/#10200 were children of the 9835 epic
    and reach master only through squash f382785be.
  • Self-contradiction between sections — the highest-yield class here as on
    chore(memory): promote strong-fit tasks-and-targets drafts from memory-pipeline for review #123. Example: 10922 asserted the ancestor walk "ensures attachments land on
    the structurally-correct owner" and then, in the same paragraph, gave the
    limitation that defeats it (the walk starts from a node found by filename, so
    two same-named files land on the same sub-doc).
  • Fixes that introduced new defects. Several commits exist only to repair
    earlier commits in this sequence — including one that broke a claim that had
    been correct, and three where a fix introduced a literal matching nothing
    (postResource('api/v1/person') when the real call takes the context first).
    This is the single most common source of findings on this branch, ahead of
    anything present in the original drafts.

The attribution sweep, run over this batch

Your closing note on #123 asked for every "this PR added X" and "on master Y"
sentence to be checked against gh pr view --json files and
git grep origin/master. That was mechanised for tasks-and-targets; it has now
been run here too, over all 48 cht-core PRs these 41 drafts reference.

63 candidates, no real defects. 53 from the file-status screen, 10 from the
symbol screen, each adjudicated by hand. They fall into three artifact classes:

  • "added a symbol in/to a file the PR modified" — "Added a Karma unit test
    in form.service.spec.ts"
    (10 hits);
  • verbs inside filenames — reports-**add**.component.ts matches "add",
    with-**extension**-libs.wdio-spec.js matches "extension" (6 hits);
  • prose about other actors — ODK adding a trigger column, Chrome dropping
    DOMSubtreeModified, the epic squash adding input.ts.

The one that needed real checking was 10180's Input.v1.UpdateReportInput,
absent from #10180's own patch. It is genuine — input.ts:36 and
local/report.ts:63 on master — and absent from that PR only because the epic
squash introduced input.ts, which the draft's Provenance section already scopes.

On convergence, and why this round exists

The previous stopping point was not converged, and the pattern is worth naming
because it will recur. Passes g19/c19 came back clean and the run ended
there. But g17g19 and c17c19 all ran after the final commit, and
c17, c18 and g18 had each found something on those same committed bytes
that nobody then acted on. Clean-at-the-end is not the same as converged when
each pass samples a different subset of claims.

Seventeen more findings came out of that, across six further commits. Rather than
list them all — they are in the commit messages — the shape is worth stating: six
of the seventeen were introduced by fixes earlier in this same sequence,
including three of my own. One (10064) I wrote into a Domain Rationale and all
three coherence passes of the next set caught it. Another (10071) had been
flagged by a pass I had already read, and I fixed a different line on the same
draft and left it — a finding read is not a finding fixed.

The three that were outstanding at the start of this round are fixed in it:

  • 10922 — the bare contact-photo.component.ts (g18).
  • 10922 — the ancestor-walk guarantee vs its own stated limitation (c17).
  • 8656 — Root Cause claimed the asMoment() fallthrough "re-parsed the raw
    input … 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 (c18).

Gate

validate-schema 95/0/3. verify-drafts --online 0 blocking, 0 warnings, 0
unverified
across all 41. ground-claims 0 ungrounded, 0 unverifiable, 0
anchor-unusable, 0 unmatched code fences
. check-coherence 0 contradictions.

The counts matter less than the sequence. Across this branch the gate has run
30 ground-claims passes and 40 coherence passes. The closing set, against
the committed bytes of e98e5ae, was four consecutive clean coherence passes
(37–40) and two clean ground passes (30–31)
, with the authenticated online audit
clean over all 41.

That bar exists because of how this corpus actually behaved. Coherence produced
findings in passes 17, 18, 20, 21, 22, 23, 25, 28, 31, 32 and 33 — almost always
after one or two clean passes. A clean pair was never proof here, and the state
this round inherited (g19/c19 clean, run ended) had three findings sitting
unactioned on exactly those bytes. Every closing set was run against frozen,
committed content, and the content was re-frozen after each fix.

One operational note that cost real time: verify-drafts --online was reporting
28–41 drafts unverified and exiting 3. That was GitHub's 60/hour anonymous limit,
not the corpus. Authenticated, the same bytes return 0 unverified and exit 0.
Anything re-running this should export a token first.

Disclosed rather than fixed: 27 stale-as-written items across 10 drafts,
concentrated in the epic-branch content (10922, 10071, 10180) whose paths
and symbols are real on their branches and absent from master. Those drafts carry
explicit "not on master" banners and a Provenance section; 10922 is
stale: true. This is what stale-as-written is for, and it is much heavier here
than on #123 because this domain's datasource work never landed as its own commits.

Merging: behind main, but no conflict

The branch is behind main (#123 landed in the meantime), so it wants an update
before merge. I expected the overlap with the already-landed drafts to be the
tricky part and it is not — I checked instead of assuming:

git merge-tree --write-tree --name-only origin/main memory/promote-forms-and-reports
#   clean; result tree 6deae2f7e1…   (exit 0, no conflict paths)
git diff --name-only $(git merge-base origin/main HEAD)..origin/main \
  -- agent-memory/domains/forms-and-reports/issues/
#   (empty)

Ten of these 41 drafts already exist on main — the hand-authored set
(8225, 8308, 8336, 8806, 9227, 9301, 9974, 10133, 10443,
10509), landed in March by #72 "categorize 10 closed CHT-Core issues (Forms
domain)"
. main has not touched any of them since, and the merge-base is #129
in July. Only this branch has modified them, so a merge is a fast-forward of
content on those paths: this branch's copies are the ones to keep, and there
is no version to choose between. Nine of the ten differ from main; 8336 is
byte-identical.

That is also why the 9227 fix matters beyond this PR — main's copy is the
March original and still says cht:luhn-check.

One thing for you, not for me

9592 — my disagreement above. If you want uniformity across the
training-cards cluster, I will re-grade it to weak.

@Hareet
Hareet requested a review from sugat009 August 12, 2026 02:47
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>

@sugat009 sugat009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 3. All four of my round-2 items are resolved, and two of the resolutions are better than what I asked for: the 9592 keep-strong justification survives full verification (the component really owns the Enketo render/save lifecycle at the anchor, the #9840 rename is real, and neither weak sibling touches form-engine code), and the data-access deferral notes are factually right and mutually consistent. The 8656 and 8740 renames are legitimate title corrections.

Requesting changes on seven items below. Every one was verified with the same protocol: quote at head, check at the draft's own anchor, check on current master, and walk every commit that touched the region in between, so each comment states whether the defect is anchor-level or drift, and includes a suggested rewording. Five are blocking under the accuracy bar (a wrong sign, a wrong mechanism, a wrong attribution, a false read/write claim, and a duplicate identity), two are wording.

One systemic suggestion: standardize a template for epic-branch work, "merged into on ; not on master". The corpus currently errs in both directions (10767 treated an epic-branch merge as shipped; 10922 here calls merged PRs unmerged), and one sentence pattern fixes the class.

## Solution

Updated the form loading code in `generate-xform.js` to not request attachments in the `_all_docs` call. Instead, attachments are loaded separately per form, and only the relevant ones (XForm XML) are fetched. PR #10248 changed 5 files in the API layer.
Dropped `attachments` from both reads: `forms.js`'s `_all_docs` call now passes only `include_docs`, and `generate-xform.js`'s `update` now calls plain `db.medic.get(docId)`. Attachments are then loaded separately, by name, per form. Only one attachment is read: the XForm XML, whose name is resolved dynamically by the new `formsService.getXFormAttachmentName(doc)` helper (literally `xml`, or any `*.xml` other than `model.xml`). `model.xml` and `form.html` are not read at all — they are the generated outputs generate-xform.js writes back onto the doc (:243, :247). Large media attachments are never loaded during startup or form processing (PR #10248). PR #10248 changed 5 files in the API layer.

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): "model.xml and form.html are not read at all" and "Only one attachment is read" are both false, and not rescueable by time-scoping. PR #10248 itself ADDED the reads: its own diff contains + formsService.getAttachment(doc._id, 'form.html') and + ... 'model.xml' inside updateAttachments, which fetches THREE attachments in one Promise.all (the XForm XML plus these two, as inputs to addGeneratedAttachments). Only two commits have touched generate-xform.js since (#10634 error logging, #10949 xsltproc hardening) and neither touches that region, so master's :254-261 is byte-for-byte what #10248 wrote. The true story is "named reads instead of bulk reads": #10248 stopped loading ALL attachments and switched to fetching exactly three by name. Suggest replacing the two quoted claims with that framing; the write-back description (:243, :247) is correct and stays.


## Problem

`npm run unit-webapp` failed for developers outside UTC-4: the `medic-xpath-extensions` mocha suite (`#difference-in-months` and every `#to-bikram-sambat()` case) baked the runner's local timezone into its expectations, so results came out off by a day or a month in e.g. NZ (+12) — tracked in #8556. This was a test-determinism problem, not a defect in the extension functions themselves; no deployed form behaviour was wrong.

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): Date.prototype.getTimezoneOffset = () => -240 emulates UTC+4, not UTC-4, so "failed for developers outside UTC-4" (summary, line 10; Problem, line 47) is wrong on both sign and scope.

getTimezoneOffset() returns UTC minus local time in minutes, and the distilled commit's own spec proves the convention: at 7202de54, webapp/tests/mocha/unit/enketo/medic-xpath-extensions.spec.js lines 24-25 stub () => -60 and assert getTimezoneOffsetAsTime returns '+01:00', lines 29-30 stub () => 60 and assert '-01:00', so -240 is '+04:00'. Scope is also over-strong: the fix changed no to-bikram-sambat expectations, those same expectations passed pre-fix in CI at UTC, and issue #8556's failures were reported from NZ (+12). The suite was timezone-dependent, passing in some zones (UTC included) and failing in others, not "passing only at UTC-4".

Suggested rewording for both places: "the suite baked timezone-dependent expectations into its cases, so npm run unit-webapp failed in some timezones (e.g. NZ +12, per #8556) while passing in others such as UTC; the fix pins getTimezoneOffset to -240 (emulating UTC+4) so results are identical everywhere."

nitpick: "every #to-bikram-sambat() case" (line 47) is over-broad. At 7202de54 that describe holds 5 conversion cases plus 11 invalid-input cases asserting an empty string, which are timezone-independent; #8556's log shows exactly the 5 conversion cases failing. "Every #to-bikram-sambat() conversion case" would be accurate.


## Problem

The cht-datasource local data context could not create reports. Consumers operating against the local (direct-database/PouchDB) adapter could not create report documents through the datasource abstraction, leaving the local context behind the intended create capability. On the remote/API side the same gap existed: the report module exposed only read operations, with no remote create implementation, API controller method, or POST route for creating reports, even though person already had a full create path on the epic branch by the time this PR landed — `Person.v1.createPerson`, a `createPerson` handler in `api/src/controllers/person.js`, and a `postResource` call for the `api/v1/person` route in `shared-libs/cht-datasource/src/remote/person.ts` are all present at this PR's parent `cab214534`. Place had none yet; that half is tracked as #10038 (PR #10099).

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 draft contradicts itself about PR #10099, and the place-create attribution at lines 59 and 75 is wrong. Line 69 has it right: #10099 ("feat(#10040): add API support to create reports", merged 2025-07-03 as d19e4e507 on the epic branch, related issue #10040) only touched the person and place create paths to align validation. Its place.ts diff swaps assertPlaceInput for validatePlaceInput on already-standing create wiring, and its place.js diff adds can_create_places to the existing createPlace handler. But line 59 says the place half "is tracked as #10038 (PR #10099)" and line 75 says place.ts's create path "was not [standing], and was added alongside this work by the same #10099". Both are false. The place create path (#10038, "To have API that can create places") was added earlier on the epic branch by PR #10065 ("feat(#10038): implement createPlace local", 169a02355) and PR #10089 ("feat(#10038): add API support to create place", 98a687a80, merged 2025-06-26), with #10094 (806456120) converting the create surface to PlaceInput. By the time #10099 landed, both person and place create paths were standing. Note line 59's "Place had none yet" is true at this draft's anchor cab214534 (no create in place.ts, local/place.ts, or api/src/controllers/place.js there), so only the parenthetical needs fixing. Suggested edits: line 59, replace "(PR #10099)" with "(local PR #10065, API PR #10089)". Line 75, replace the clause after "mirroring person.ts" to say both the person and place create paths were already standing on the epic branch when #10099 landed (place via #10065 and #10089), and #10099 aligned their validation while adding the report path. Line 121 is accurate as written and needs no change.


## Testing

Updated the existing `tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js` WebdriverIO spec for the new relevance/widget behaviour; that is the only test change the epic squash carries. Note that #8740 is an epic child merged into the `7599-uplift-enketo-7` branch and squashed as #8528 (`314e79061a`), so its own commit sha is no longer resolvable in a clone — verify file claims against the squash, not `source_sha`.

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): The "only test change the epic squash carries" half checks out, but the characterization does not. Squash 314e79061a's entire test diff is +2/-2 in tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js, and both lines only change excludingEvery('instanceID') to excludingEvery(['instanceID', 'deprecatedID']) in the edit-report assertions, needed because enketo-core v7 populates meta/deprecatedID on edited submissions. That is edit-metadata fallout of the uplift, not relevance/widget coverage. PR 8740's actual widget-behavior test edits, removing the browser.keys('Escape') datepicker workaround in tests/e2e/cht-form/default/death-report.wdio-spec.js and tests/page-objects/default/enketo/delivery.wdio.page.js, netted to zero in the squash (the workaround is absent from 314e79061a^), so they are invisible there. Suggest rewording to: "The epic squash's only test change is tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js, which adds 'deprecatedID' to two excludingEvery() exclusion lists because enketo-core v7 sets meta/deprecatedID on edited submissions. PR 8740's own widget-behavior test edits (Escape-workaround removals in death-report.wdio-spec.js and delivery.wdio.page.js) cancelled out within the epic branch and do not appear in the squash."


## Problem

> **Not on `master` (as of 2026-08-09).** Neither PR #10922 nor PR #11116 has been merged. The commits live only on the epic branches `origin/10700-photo-capture`, `origin/10700-photo-capture-in-sub-contacts-and-reports` and `origin/5.1.2-FR-attachments-for-subcontacts`; none of them is an ancestor of `origin/master`, and neither `resolveOwnerDoc` nor `findFileNodeByFilename` exists in `webapp/src/ts/services/enketo.service.ts` on `master`. Everything below describes the state of those branches, not shipped behaviour. (The frontmatter `source_sha` `cc34e08664…` and the current branch head `0df57c664…` are two rebased copies of the same squash.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue (non-blocking): The banner's core scoping is verified correct: neither cc34e08664 nor 0df57c664 is an ancestor of origin/master (checked at 6eb2ac0f1), resolveOwnerDoc/findFileNodeByFilename appear nowhere on master (and never have, git log -S is empty), and the two shas really are rebased copies of the same #10922 squash (identical git patch-id --stable 530a4a2a1137, same author, date, and subject). Three details are wrong, though:

  1. "Neither PR #10922 nor PR #11116 has been merged" is false. Both are merged=true on GitHub: #10922 merged 2026-05-19 into 10700-photo-capture-in-sub-contacts-and-reports (its squash merge commit is exactly the frontmatter source_sha cc34e08664), and #11116 merged 2026-05-29 into 5.1.2-FR-attachments-for-subcontacts (squash e88c88361). Reword to "merged only into the epic branches, not into master", and update the frontmatter summary's "NOT YET MERGED" and the "unmerged epic branch work" concept to match.
  2. "the current branch head 0df57c664" is inaccurate. No ref points at that sha; it is an interior commit of origin/10700-photo-capture, whose head has been bade9485b since 2026-07-10, before this banner's "as of 2026-08-09" stamp. The sha that IS a current branch head is cc34e08664 itself (tip of origin/10700-photo-capture-in-sub-contacts-and-reports). Suggest: "the source_sha cc34e08664 (head of origin/10700-photo-capture-in-sub-contacts-and-reports) and 0df57c664 (a rebased copy on origin/10700-photo-capture) are the same squash".
  3. "The commits live only on the epic branches [all three]" overstates. Neither sha is contained in origin/5.1.2-FR-attachments-for-subcontacts; that branch carries the equivalent enketo.service.ts changes via the #11116 squash e88c88361 instead.


## Design Choices

Pushed the index-shape decision down into cht-datasource so future changes to the freetext indexes are invisible to the validation lib — the stated goal of #9653. Callers therefore no longer choose a view or a key range: cht-datasource decides internally whether a qualifier hits the keyed offline view, a range scan, or Nouveau, based on whether the freetext contains a `:` separator. Delegating rather than rewriting the validators kept backwards compatibility with existing data and configuration.

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 ':' separator does not route between Nouveau and the offline path, so this sentence misattributes the routing. On current master, getUuidsPage (shared-libs/cht-datasource/src/local/report.ts) chooses Nouveau vs offline views via useNouveauIndexes(medicDb) (src/local/libs/nouveau.ts), which returns true when the _design/medic-offline-freetext ddoc is absent, i.e. when running server side. The ':' check (isKeyedFreetextQualifier, src/qualifier.ts) only selects keyed exact-match vs prefix range scan inside the offline view path, plus exact_match vs fuzzy syntax inside the Nouveau query. At this draft's anchor (e4d7bdc8, the #9755 merge), Nouveau was not in this code path at all: getUuidsPage queried medic-client/reports_by_freetext, with ':' selecting keyed vs range (Nouveau replaced the views later, in f1bdfc07c, feat(#9542), PR #10201). Suggested rewrite of the second sentence: "Callers therefore no longer choose a view or a key range: cht-datasource decides internally which index a qualifier hits. It uses Nouveau when running server side (no medic-offline-freetext ddoc in the db) and the offline reports_by_freetext view otherwise, doing a keyed lookup when the freetext contains a ':' separator and a prefix range scan when it does not." For the record on the round-2 item: the whitespace-fallback claim was fully removed from all six sections in b82840f, and no residue remains at head; the remaining defect is this replacement sentence, which that same commit introduced.

category: feature
domain: forms-and-reports
domainFit: strong
issueNumber: 8074

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 draft duplicates the identity of a landed contacts draft. Lines 2 and 6 carry id: cht-core-8074 / issueNumber: 8074, which agent-memory/domains/contacts/issues/8074-filter-contact-search-by-parent.md has owned on main since #79 (a0108f7) and which #132 is actively correcting (55dbd0a, 1973657). The key itself is right, cht-core PR 8759's closingIssuesReferences is exactly #8074, and 25e7558 deterministically re-keyed this file from cht-core-8759 to cht-core-8074. But that relink made it a second memory for one issue: the two files share the identical 7-file Related Files set and describe the same change, and they have already diverged once (the set-intersection description was fixed only on the contacts side in 55dbd0a). The 849aa01 collapse pass enforced "one memory per issue" and "no duplicate issueNumbers" within forms-and-reports only, so this cross-domain pair slipped through, the same blind spot 55dbd0a documented for the 8034/data-sync pair.

Suggestion: remove this file from #122 and fold its unique content into the contacts canonical (the specific unit/e2e spec paths in Testing, and the with-same-parent to descendant-of-current-contact naming rationale in Design Choices), mirroring how 849aa01 moved the #9835 pair to "the contacts canonical that owns the issue". Do not re-key it back to 8759; that is a PR, not an issue, which is exactly what 25e7558 fixed, and PR 8759 closes no other issue it could be keyed to. If the team instead decides forms-and-reports should own this memory, that is a relocation of the single canonical draft (a corpus decision touching a landed file), never a second copy. Given this is the second cross-domain duplicate to escape the per-domain gate, a corpus-wide issueNumber uniqueness check in validate-schema would close the class.

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