Repository navigation
Conversation
Consolidated findings from reviewing the 8 chore(memory): promotion PRs (#120-#132): dominant PR-number-as-issue-number defect (~60/107 drafts), missing de-duplication, static frontmatter, and prose grounding gaps. Includes root-cause analysis, prioritized roadmap, a 3-layer evaluation strategy, and a 20-scenario regression catalog.
sugat009
left a comment
There was a problem hiding this comment.
Comprehensive review of the post-mortem, every claim re-checked against the live GitHub API and source, unbiased. The core is solid and this is a valuable doc: the prose-vs-plumbing thesis, every source-code reference, all 15 PR->issue resolutions, and the backport/cross-domain analysis all hold up. A handful of inline corrections before it becomes the reference gating #135/#104: two factual errors (the resolution priority chain is inverted vs the shipped code; the 10623 hallucination example is wrong), and three staleness/labeling items (#129's shipped scope, the roadmap already living in #138, and the "60/107 corpus-wide" scope). All non-blocking, all inline.
| ## 4. Pipeline improvements that address multiple issues | ||
|
|
||
| 1. **Resolve the canonical issue with a priority chain; stop aliasing the PR number.** | ||
| `(1)` body `Fixes/Closes/Resolves` -> `(2)` CHT title `type(#N):` -> `(3)` GraphQL |
There was a problem hiding this comment.
issue: The recommended resolution order here (body Fixes/Closes/Resolves -> title type(#N): -> closingIssuesReferences) is inverted relative to the shipped code. issue-linkage.ts (byte-identical on #129 and #138) orders closing-ref -> title -> body as explicit "descending authority", with closingIssuesReferences the most authoritative and linkedIssues[0] canonical. A closing-ref is GitHub's authoritative link, so it should win over an author-typed body keyword, not lose to it. In the divergence case (title #N, empty closing-refs, body Closes #M) this doc picks #M while the code correctly picks the authoritative signal. The same ordering flows into the Part 2 eval chain (L213-214) and the Part 3 title vs body precedence fixture (L254), which would then encode the wrong canonical issue. Suggest flipping to closingIssuesReferences -> title -> body throughout to match the implementation.
There was a problem hiding this comment.
Good catch — verified against issue-linkage.ts on the #129/#138 branches (collectLinkedIssueRefs pushes closing-ref → title → body, "descending authority", first entry canonical). Flipped the chain to closingIssuesReferences > title > body throughout: section 4.1, the Part 2 unit chain, the Part 3 fixture (now "title outranks body", issueNumber=8026), and the Part 4 precedence line. Fixed in ba5b5ce.
| > `relatedFiles` and `entities` MUST be chosen only from the Files changed list above. Do not | ||
| > infer, guess, or invent paths. If a file is not in that list, do not include it. | ||
|
|
||
| *Fixes:* hallucinated paths (#123/10623 - `message.pipe.ts`, `reducers/tasks.ts` not in the PR). |
There was a problem hiding this comment.
issue: This example does not hold up. PR 10623 does change webapp/src/ts/pipes/message.pipe.ts and webapp/src/ts/reducers/tasks.ts (plus reducers/global.ts and their specs) per gh api repos/medic/cht-core/pulls/10623/files, so the draft's file list is accurate, not hallucinated. Since 10623 is the flagship "hallucinated paths" case (also cited in the Part 3 fixture at L258 and the appendix at L399-400), it should be dropped or swapped for a real case. The other prose examples (Slack L341, "cf. Seed 4" L382) do check out, so the point survives, just not this instance.
There was a problem hiding this comment.
Confirmed via gh api .../pulls/10623/files: message.pipe.ts and reducers/tasks.ts are both in the real PR. But the draft (10623-feat10577-….md) also lists services/user-contact.service.ts, which is not in the PR — so 10623 is still a valid hallucination, just misdescribed. Kept it as the exemplar and corrected the file facts at L118/L267/L409. Fixed in ba5b5ce.
|
|
||
| PR #129 (data-sync) already shipped a **partial fix** (`gh-classify.ts` using the `pull_request` | ||
| API key to tell issues from PRs, plus `issue-linkage.ts` and a one-off `relink-issues.ts`), but | ||
| its "affected" predicate (`issueNumber === source_pr`) was narrower than the bug (missed the |
There was a problem hiding this comment.
issue (stale): This understates what #129 already shipped. On the #129 branch: the nested 10399 -> 10182(PR) -> 10183 case is handled and regression-tested (relink-issues.spec.ts:96 asserts to === 10183); title parsing is wired into the forward path (scraper.ts:305 collectLinkedIssueRefs(prTitle, ...)); and the ?? pr.prNumber alias is gone from the forward write path (distiller.ts:268 uses linkedIssues[0]?.number and throws/flags when absent). Of the three gaps listed, only deduplication is genuinely still open in #129. Suggest narrowing this to dedup.
There was a problem hiding this comment.
Verified on #129 head (58e1e0c): scraper.ts wires collectLinkedIssueRefs(prTitle, …), distiller.ts:268 dropped the ?? pr.prNumber alias and flags no-issue PRs, and relink-issues.spec.ts:96 covers the nested 10399→10182→10183 case. Narrowed the paragraph to deduplication as the one open gap. Fixed in ba5b5ce.
|
|
||
| | Rank | Item | Impact | Effort | Rationale | | ||
| |:---:|---|:---:|:---:|---| | ||
| | **1** | Complete the issue-resolution chain (title `type(#N)` + `closingIssuesReferences`) and remove the `?? pr.prNumber` alias; set `issueNumber` null + flag when unresolved | high | med | Repairs the ~60/107 mislink that corrupts the corpus key and blocks #135. Extends #129's existing `gh-classify.ts`/`issue-linkage.ts`. | |
There was a problem hiding this comment.
note: Most of this roadmap is already implemented in the open #138 (same author). Ranks 1-3 and 5-8 map to dedup.ts (ciGuardReason, dedupeByIssueId + source_prs[]), reconcile.ts (hallucinationRate), the CONSTRAINTS block, stripBoilerplate + ISSUE_BODY_LIMIT_EXPANDED, and computeConfidence. Worth reframing as "tracking #138" so it reads as in-flight rather than net-new. One caveat: rank 4's specific design (nullable issueNumber, /pull URLs, cht-core-pr-<n>) is contradicted by #138, which keeps issueNumber required (distiller.ts:329 still emits /issues/${issueNumber}) and flags/skips no-issue PRs instead. Rank 4 should be rewritten to match the require-an-issue-or-skip approach that actually shipped.
There was a problem hiding this comment.
Agreed. Added a status note framing the roadmap as tracking #138 (ranks 1-3, 5-8 → dedup.ts/reconcile.ts/CONSTRAINTS/stripBoilerplate/computeConfidence). Rewrote rank 4 and section 4.2 to the shipped require-an-issue-or-skip approach (issueNumber stays required, no-issue PRs flagged), and fixed the Part 4 invariant. Fixed in ba5b5ce.
| > `fixes|closes|resolves #N`, but CHT encodes the issue in the **PR title** `type(#N):`, which is | ||
| > never parsed. So for the common case `linkedIssues` is empty, the `?? pr.prNumber` fallback | ||
| > fires, and the memory's primary key names the **merge PR** instead of the resolved issue. | ||
| > **~60 of 107 drafts corpus-wide** are affected. The `/issues/N -> /pull/N` GitHub redirect makes |
There was a problem hiding this comment.
nitpick: "~60 of 107 drafts corpus-wide" is really the four fully-reviewed domains, not the corpus. The 8-domain table above sums to 255 memories; 60/107 is exactly contacts+auth+config+data-sync (44+39+10+14 = 107; mislinks 31+19+4+6 = 60). Section 1.1 (L63) then lists tasks (15/35) in the same sentence but still totals 60/107. Suggest relabeling "60/107 across the four fully-reviewed domains" (corpus-wide would be higher, out of 255) to keep the headline stat precise.
There was a problem hiding this comment.
Relabeled: "~60 of 107 across the four fully-reviewed domains (contacts/auth/config/data-sync)" at L41, and reworked the 1.1 line so 60/107 is the four domains with tasks/messaging/forms/infra called out as additional. Fixed in ba5b5ce.
| #121; #135 (de-dup consumer); cross-domain dupes with #131 (#9835/#9065/#6543). | ||
| - **[incorrect | sugat009]** 31/44 drafts: `issueNumber`/`issueUrl`/`id` name the merge PR. e.g. | ||
| 10043 stores `issueNumber:10043` (PR `feat(#10036):...`) but the real issue is #10036. | ||
| - **[duplication | sugat009]** #10038 x5, #10036 x3, #8985 x2 (9027 + its 4.7.x backport 9098), etc. |
There was a problem hiding this comment.
nitpick (minor, non-blocking): A few small accuracy items in the appendix/fixtures: #10036 is x4 not x3 (merged PRs 10070/10061/10056/10043). "all 44 ... confidence: medium / related_issues: []" (L345) holds for the distilled drafts but not 4 legacy subDomain-schema drafts that carry neither field. Two Part 3 fixture titles (8843, 8675 at L252/255) are paraphrased rather than the verbatim PR titles. And CouchDB 9960/10014 (L421) close different issues (9882 vs 10027), so they are duplicated effort, not a single-issue backport pair.
There was a problem hiding this comment.
All four fixed in ba5b5ce: #10036 → x4 (10070/10061/10056/10043); noted the 4 legacy subDomain-schema drafts lack confidence/related_issues; used the verbatim 8675/8843 titles; and clarified CouchDB #9960/#10014 close different issues (#9882 vs #10027) — duplicated effort, not a backport pair.
- Flip issue-resolution precedence to match shipped issue-linkage.ts (closingIssuesReferences > title > body, descending authority) - Fix PR 10623 hallucination facts: only user-contact.service.ts is invented; message.pipe.ts/reducers/tasks.ts are real - Narrow #129 open gaps to deduplication (title-parse + forward-write path + nested-chain case already shipped and tested) - Reframe roadmap as tracking #138; rewrite rank 4 to the shipped require-an-issue-or-skip approach - Relabel 60/107 as the four fully-reviewed domains, not corpus-wide - Appendix: #10036 x4, subDomain-schema draft caveat, verbatim 8675/8843 titles, CouchDB 9960/10014 close different issues
sugat009
left a comment
There was a problem hiding this comment.
Fact check of the document against the code on main and the GitHub record. Each finding was verified twice: by an independent adversarial pass, then by direct command output. The analysis is valuable, and much of it was accurate for the 2026-07-01 date. The 15 inline comments give the corrections.
Two rendering defects have no inline anchor: a line-initial - at lines 249-250 splits a bold span and creates a stray bullet, and lines 3-9 render as one paragraph.
| `medic/cht-core` PRs. | ||
| **Method:** One analysis agent per PR (read PR body, comments, reviews, inline review threads, | ||
| each distilled `.md`, and spot-checked source issues against the live cht-core API), then a | ||
| synthesis pass. ~107 distilled memories reviewed across the 8 domains. |
There was a problem hiding this comment.
issue (blocking): The count contradicts the table below.
The table sums to 255 (44+39+10+14+35+47+49+17). The number 107 is the sum of the four fully-reviewed domains only. Line 65 confirms this scope: tasks, messaging, forms, and infra "add more on top". Suggested fix: "255 distilled memories across the 8 domains, ~107 fully reviewed in four of them". The headline ~60/107 rate then reads correctly as a four-domain figure.
| | #132 | contacts | 44 | 7 | sugat009 (CHANGES_REQUESTED) | | ||
| | #131 | authentication | 39 | 7 | sugat009 (CHANGES_REQUESTED) | | ||
| | #130 | configuration | 10 | 6 | sugat009 | | ||
| | #129 | data-sync | 14 | 10 | sugat009, Hareet (partial fix already shipped) | |
There was a problem hiding this comment.
issue: The "Reviewer" column holds participants, not reviewers.
GitHub shows Hareet authored all 8 promotion PRs and submitted no reviews on any of them before this document's date. All formal reviews to 2026-07-01 are from sugat009. The cells match the real participation on that date, so the data is correct. The header is not. Fix: rename the column to "Participants", or list only review submitters. Note: Hareet later submitted one COMMENTED review, on #123 on 2026-07-21, and #123 is the one row that omits him.
| | Failure class | Prevalence | PRs | Root cause | | ||
| |---|---|---|---| | ||
| | **PR number written into issue-identity fields** (`id`/`issueNumber`/`issueUrl`) | ~60/107; every seeder | all | `scraper.fetchLinkedIssues` misses title `type(#N):` + `closingIssuesReferences`; `distiller.ts:267` `?? pr.prNumber` fallback then derives id/URL from it | | ||
| | **Duplicate memories** - backport cherry-picks + multi-PR epics each yield a file | clusters per domain (7/47 forms = 3 fixes; 6/17 messaging = 3 fixes; #10038 x5) | 132,131,129,123,122,121,120 | No dedup stage; backport markers (`cherry picked from commit`, trailing `(#M)`, `baseRefName != master`) captured but unused | |
There was a problem hiding this comment.
issue: baseRefName is not captured.
The scraper's --json field list does not request it, and ScrapedPR has no such field. It has zero occurrences in src/ at this document's code and on main. #138 adds it, which confirms the gap. The other two markers (body cherry-pick marker, title (#M) suffix) are captured. Scope "captured but unused" to those two, and note that baseRefName needs a scraper change plus a type change (also lines 165, 326).
| |---|---|---|---| | ||
| | **PR number written into issue-identity fields** (`id`/`issueNumber`/`issueUrl`) | ~60/107; every seeder | all | `scraper.fetchLinkedIssues` misses title `type(#N):` + `closingIssuesReferences`; `distiller.ts:267` `?? pr.prNumber` fallback then derives id/URL from it | | ||
| | **Duplicate memories** - backport cherry-picks + multi-PR epics each yield a file | clusters per domain (7/47 forms = 3 fixes; 6/17 messaging = 3 fixes; #10038 x5) | 132,131,129,123,122,121,120 | No dedup stage; backport markers (`cherry picked from commit`, trailing `(#M)`, `baseRefName != master`) captured but unused | | ||
| | **Cross-domain duplication** - same issue promoted into two domain seeders | >=3 issues (#9835/#9065/#6543) | 132,131 | `filter.ts` classifies each PR independently; no global dedup key across seeders | |
There was a problem hiding this comment.
issue (blocking): The dedup design misses collisions with the merged corpus.
The design at lines 162-166 ("across ALL seeders, before promotion") compares only drafts inside the promotion batch. Two #123 drafts collided with memories already on main before this document's date: 10432 vs cht-core-10344 (on main since 2026-06-04) and 9975 vs cht-core-9974 (on main since 2026-03-31). Batch-internal dedup cannot catch these. Also, #6543 had a third seeder in #123 (9099-feat6543), beyond the two this row names. Fix: check the promotion set against the merged corpus, and extend the line 279 fixture past the 132/131 pair.
|
|
||
| *Fixes:* fabricated "Slack" (#132/9311), process noise (#120/#130), hedged e2e narrative | ||
| (#120/10442). | ||
| 3. **Clean `domainReasoning` (field description, ~line 188).** |
There was a problem hiding this comment.
issue: Prompt fixes 3 and 4 must also edit DRAFT_SHAPE, or CLI mode keeps the old text.
The field descriptions exist twice: in buildPrompt (~188, ~199) and in DRAFT_SHAPE (distiller.ts:80, :91). CLI mode appends DRAFT_SHAPE after the prompt (structured-cli.ts:52-55), so the old wording lands last. The rootCause copy in DRAFT_SHAPE (<why it was wrong>) is the vaguer of the two. #138 currently edits only buildPrompt, so this miss is already in that diff. Name both edit sites here.
| | nested PR chain | draft `10399-fix10182` where 10182 is a PR (`fix(#10183)`), 10183 the real issue | `issueNumber=10183` after the PR->issue hop; others untouched (idempotent) | | ||
| | multi-issue body prefers title | `{prNumber:9311, title:'feat(#9193)...', body links #9241/#9237/#9238}` | `issueNumber=9193`; body Related Issues lists the rest | | ||
| | relatedFiles grounded | `{prNumber:10623, fileList has message.pipe.ts + reducers/tasks.ts, no services/user-contact.service.ts}` | `relatedFiles subset of fileList`; `services/user-contact.service.ts` rejected | | ||
| | confidence reflects quality | backport draft 9555 + a fallback-to-PR draft | `confidence 'low'` for backport/fallback vs `'high'` for a verified rich draft | |
There was a problem hiding this comment.
issue: The expected value 'high' is not reachable.
computeConfidence in #138 is typed 'medium' | 'low', and its docstring reserves 'high' for human-reviewed entries. Line 307 of this document also says the plan is only to downgrade. Change the expectation to 'medium' for the rich draft. Also, "a fallback-to-PR draft" is a state the merged code can no longer produce; use a flagged no-issue PR as the low-side fixture instead.
| | multi-issue body prefers title | `{prNumber:9311, title:'feat(#9193)...', body links #9241/#9237/#9238}` | `issueNumber=9193`; body Related Issues lists the rest | | ||
| | relatedFiles grounded | `{prNumber:10623, fileList has message.pipe.ts + reducers/tasks.ts, no services/user-contact.service.ts}` | `relatedFiles subset of fileList`; `services/user-contact.service.ts` rejected | | ||
| | confidence reflects quality | backport draft 9555 + a fallback-to-PR draft | `confidence 'low'` for backport/fallback vs `'high'` for a verified rich draft | | ||
| | host-anchored closing-ref URL | body link `https://github.com/attacker/medic/cht-core/issues/1` | rejected - only `startsWith https://github.com/medic/cht-core/issues/` passes (guards #129's relink fix) | |
There was a problem hiding this comment.
suggestion (non-blocking): The input label points at the wrong mechanism.
The success criterion names the startsWith guard in sameRepoClosingRefs, which only sees sidebar metadata, never body text. A body link cannot reach it: bodyIssueRefs needs a Fixes/Closes/Resolves keyword, and its pattern cannot match the four-segment attacker path at all. Change "body link" to "closingIssuesReferences entry". Note: main already ships this exact test with this URL (test/scripts/issue-linkage.spec.ts:74-85), so this row points at an existing guard rather than adding a new test.
| #135, #119. | ||
| - **[incorrect | sugat009]** 4/10 mislinked (10198->#8026, 10278->#8027, 8722->#8075, 9407->#9406); | ||
| verified against the live API; pipeline-wide 60/107. | ||
| - **[pipeline | self]** root cause = scraper body-only regex; the correct number is in the title |
There was a problem hiding this comment.
issue: The root cause is incomplete. The body also had the correct reference.
PR 10198's body contains Closes medic/cht-core#8026. The regex accepts only a bare #N or a full URL after the keyword, so the qualified owner/repo#N form silently drops (verified by execution; the bare form matches). The number was not available only in the title. The gap is still in main's bodyIssueRefs. Add this form to the remediation list, next to the Fixes: #N colon form from the 10914 case.
| narrower than the bug - missed nested `10399 -> 10182(PR) -> 10183`. | ||
| - **[duplication | self]** #10792 promoted 3x (10793/10798/10799), all `id: cht-core-10792`. | ||
| - **[missing-info | self]** 10914 Root Cause vague - issue #10912's `docIds.add(...keys)`/`Set.add` | ||
| single-arg mechanism was truncated away by `ISSUE_BODY_LIMIT=500` (PR body was just `Fixes: #10912`). |
There was a problem hiding this comment.
issue (blocking): The cause here is a linkage miss, not truncation.
The PR body reads Fixes: #10912 with a colon. The scraper regex requires whitespace directly after the keyword, so it did not match (verified by execution). linkedIssues was empty, and issue #10912 was never fetched. Proof: the as-distilled draft (commit bf92766) has issueNumber: 10914, so the PR-number fallback fired. Truncation cannot remove text that was never fetched.
Two follow-ups. First, the truncation scenario is now live on main: the merged linkage chain resolves #10912 (sidebar and title), and the mechanism sentence sits at character 589, past ISSUE_BODY_LIMIT=500. Keep rank 6, and recast this case's history as a linkage miss (also lines 87-89, 105, 178, 204, 287, 313-316). Second, no remediation names the keyword-colon form, and main's bodyIssueRefs still misses it. Small correction: the PHI comment is 343 characters, not ~246 (lines 87, 178, 314).
| 3.5.0 #9960/#10014 close *different* issues - #9882 vs #10027 - so they are duplicated effort, not | ||
| a single-issue backport pair.) | ||
| - **[pipeline | self]** no cross-PR dedup anywhere. | ||
| - **[schema | self]** ~12/49 close no issue (8693, 10689, 10488) -> PR number labeled as an issue. |
There was a problem hiding this comment.
issue: Both the count and the example are wrong.
Only 2 of 49 infra PRs truly close no issue: 8693 and 10689. PR 10488 closes #10481, as line 431 of this document says, so the two remediations for it conflict. PR 8996 closes #9118 through a body keyword. Separately, the measured mislink rate in this batch is 23 of 49, so ~12 understates the defect. Write "2/49 close no issue (8693, 10689)" and add the 23/49 mislink figure.
Summary
Adds
docs/memory-pipeline-postmortem.md- the consolidated post-mortem of the first productionrun of the memory distillation pipeline (
scraper -> filter -> distiller -> open-review-pr), runagainst merged
medic/cht-corePRs and surfaced as the 8 openchore(memory):promotion PRs(#120, #121, #122, #123, #129, #130, #131, #132).
~107 distilled memories were reviewed, spot-checked against
the live cht-core API.
Key finding
Distillation prose is good; the failures are in plumbing. One defect dominates:
What's in the report
Related
Test plan