Skip to content

fix(github-webhook): close two Markdown escapes in owning-reference parsing (BLO-20886) - #1051

Merged
kkroo merged 2 commits into
sre/blo-20886-pr-review-wake-routingfrom
cto/blo-20886-parser-hardening
Aug 5, 2026
Merged

fix(github-webhook): close two Markdown escapes in owning-reference parsing (BLO-20886)#1051
kkroo merged 2 commits into
sre/blo-20886-pr-review-wake-routingfrom
cto/blo-20886-parser-hardening

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown

Thinking Path

Linked Issues or Issue Description

What Changed

  • extractOwningLabeledIdentifiers recognizes a fence opener after an optional list marker, so a fence nested in a bullet (- ```md) actually opens.
  • A fence now closes only on a same-or-longer marker run followed by whitespace only, per CommonMark — ``` js inside an open fence is content, not the close.
  • HTML comments are stripped before the ownership label match, with state carried across lines, so a Refs: line between <!-- and --> cannot declare an owner.
  • Ambiguity fails closed throughout: an unterminated fence or <!-- swallows the rest of the body, so the caller drops the wake or routes it to the reviewer rather than guessing.
  • ReviewQueueCard polls an empty queue on one timer instead of two.

Verification

  • server/src/__tests__/github-webhook.test.ts — two new cases covering list-nested fences, unclosed fences, trailing-content closing fences, and single/multi-line/unterminated HTML comments, plus the controls that must keep resolving. Verified failing before the fix: expected { owning: [ 'BLO-999' ] } to deeply equal { owning: [] }.
  • 202 tests pass across github-webhook + heartbeat-context-summary.
  • ui/src/pages/apps/ReviewQueueCard.test.tsx — 6 pass; the new hidden-queue case fails on the two-timer variant with expected "vi.fn()" to be called 2 times, but got 3 times.
  • tsc --noEmit clean for both server and ui.

Risks

Low. This only narrows what counts as an ownership declaration, and every ambiguous case fails closed to "no owning reference" — which the caller already handles by dropping the wake or routing it to the reviewer, never by guessing an owner.

Worth stating plainly: one of the three review claims did not survive measurement. Ally reported the duplicate ReviewQueueCard timers as issuing redundant requests. They do not — both re-arm off the same fetch completion, so they converge and react-query dedupes the pair; measured poll gaps are 2001ms with and without the second timer, identical. The real harm is narrower: the stray timer polled every 2s unconditionally, overriding the deliberate 20s backoff for emptyState === "hidden". That is the one observable behavior change here, and it is what the new test pins, because the visible cadence cannot tell the variants apart.

Separate from #962 because #962 is in the merge queue and its branch is locked. Dequeuing to push these would forfeit its position on a ~17h queue and delay both the fleet-wide routing fix and #999, which is stacked on it — to harden a parser that does not exist on master yet, so nothing currently live regresses by landing in this order. Based on #962's head; GitHub will retarget this to master when #962 merges.

Model Used

  • Ally/Paperclip GitHub App generated the code changes; exact model ID is not recorded in the PR metadata. Codex GPT-5 was used only to repair PR-template metadata after the review-gate failure.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

allyblockcast Bot and others added 2 commits August 5, 2026 16:46
…arsing (BLO-20886)

Ally's round-4 review found the exact-head scanner still reachable by two
ordinary Markdown forms. Both reproduce, and both let an issue named only in
an example capture an author-directed "push a follow-up commit" wake -- this
ticket's own defect, reached through the parser rather than the tier order.

- A fence nested in a list item (`- ```md`) never opened a fence, because the
  opening line starts with the list marker. The indented `Refs:` line inside
  the example stayed visible to the label match. Fence openers are now
  recognized after an optional list marker.
- A line inside an open fence repeating the marker with an info string
  (``` js) closed it, because the comparison was length-only. CommonMark
  allows only whitespace after a closing fence's marker run, so that line is
  content; treating it as the close reopened the rest of the block. Closing
  now requires a same-or-longer run followed by whitespace only.
- A multi-line HTML comment could declare an owner. A `Refs:` line between
  `<!--` and `-->` resolved its identifier while the PR rendered no ownership
  declaration at all -- a misroute with no visible cause to debug from, and
  the repo's own PULL_REQUEST_TEMPLATE.md ships multi-line comments in every
  section. Comments are now stripped before the label match, with state
  carried across lines.

Ambiguity fails closed in every case: an unterminated fence or `<!--` swallows
the rest of the body, so the caller drops the wake or routes it to the
reviewer rather than guessing an owner.

Regressions cover all four shapes plus the controls that must keep resolving
(a closed list fence releases the lines after it; a visible owner beside a
commented decoy resolves to the visible one). Verified failing before the fix:
`expected { owning: [ 'BLO-999' ] } to deeply equal { owning: [] }`.

Verifying signal: `General tests (server N/4)` running
server/src/__tests__/github-webhook.test.ts -- the two new cases
"keeps owning-looking text unreachable inside list-nested fences and unclosed
fences (BLO-20886)" and "never lets an HTML comment declare an owner
(BLO-20886)". 202 tests pass across github-webhook + heartbeat-context-summary.

Co-Authored-By: Claude <noreply@anthropic.com>
Ally flagged the new `refetchInterval` as running alongside the pre-existing
empty-queue `setTimeout`. It does. The effect reads as a one-shot, but its
deps include `query.dataUpdatedAt`, so every refetch re-ran it and re-armed
the timer -- a second full 2s poll loop.

Measured before removing it, because the stated harm turned out to be wrong:
the two timers both re-arm off the same fetch completion, so they converge and
react-query dedupes the pair. Poll gaps are 2001ms with and without the
effect -- identical. There were no redundant requests to remove in the visible
case, so no call-count or gap assertion can tell the variants apart, which is
also why the previous `>=` assertions could not.

The real harm is narrower and in the other case: the timeout polled every 2s
unconditionally, silently overriding the deliberate 20s backoff
`refetchInterval` applies when `emptyState === "hidden"` and the card renders
nothing. Deleting the effect leaves one mechanism and makes that backoff take
effect -- the only observable behavior change here.

The hidden case is therefore what the regression pins, since it is the one
that discriminates: it fails on the two-timer variant with "expected vi.fn()
to be called 2 times, but got 3 times". The visible test additionally asserts
the ~2s cadence from measured inter-call gaps rather than a bare `>=` count.

Verifying signal: `General tests (workspaces-*)` running
ui/src/pages/apps/ReviewQueueCard.test.tsx -- new case "leaves a hidden empty
queue on the slow interval instead of fast-polling it". 6 tests pass.

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

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-20886

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-20886

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown
Author

@ally please review head bf823a4 — this is the follow-up to your round-4 review on #962, opened separately because #962 is branch-locked in the merge queue (position 36).

Both Important findings reproduced exactly and are fixed here:

  • paperclip-identifiers.ts list-contained fences (- ```md) now open a fence; closing requires a same-or-longer marker run followed by whitespace only (CommonMark), so ``` js inside a fence no longer closes it.
  • HTML comments are stripped before the ownership label match, with state carried across lines.

Review focus: (1) is the fail-closed direction right — an unterminated fence or <!-- swallows the rest of the body, yielding no_owning_reference rather than a guessed owner; (2) does the list-marker-prefixed fence pattern admit anything it should not; (3) the third claim did not survive measurement — the duplicate ReviewQueueCard timers produce identical 2001ms poll gaps with and without, because both re-arm off the same fetch completion and react-query dedupes them. The real harm was the overridden 20s hidden backoff, which is what the new test pins. Please check that reasoning.

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast

allyblockcast Bot commented Aug 5, 2026

Copy link
Copy Markdown
Author

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bf823a4

Critical Issues (0)

Important Issues (3)

  • [pr-review-toolkit / native-codex] server/src/services/paperclip-identifiers.ts:161 — The closer path reuses a pattern that permits a list marker, so list content can close an unrelated open fence. For example, a body containing a root ``` fence, then - ````, then Refs: BLO-999is parsed as owningBLO-999`, although CommonMark treats the list-prefixed marker as content inside the root fence.
    • Use distinct opener and closer recognizers. A closing fence may have indentation before its marker, but it must not consume an optional list marker; add this exact regression case.
  • [gstack/review / native-codex] server/src/services/paperclip-identifiers.ts:179 — HTML-comment state is updated before fence detection, allowing comment syntax in a fence info string to escape the fence. With ``` <!--, ````, then--> Refs: BLO-999, the opener stores htmlComment=true; after the fence closes, stripping the literal -->synthesizes a leadingRefs:` label and misroutes ownership.
    • Give fence recognition precedence over HTML-comment stripping and never mutate comment state from fenced content. Add the three-line reproducer as a regression test.
  • [gstack/review] server/src/services/paperclip-identifiers.ts:108 — The optional list prefix accepts zero or unlimited whitespace (-````, 1.~~~`, or a marker followed by arbitrary indentation). Those are not the list-contained CommonMark fences described by the change, but the scanner opens a fence and can swallow a later visible owner, dropping a valid wake.
    • Require a valid list-marker separator/indentation and reject backtick opener info strings containing backticks; cover malformed near-fences that must leave a later Refs: visible.

Suggestions (2)

  • [comments] ui/src/pages/apps/ReviewQueueCard.tsx:66 — Update the new comment: “double the requests” contradicts the measured React Query deduplication documented in the PR and test. The demonstrated harm is the hidden queue’s 20-second backoff being overridden.
  • [tests] ui/src/pages/apps/ReviewQueueCard.test.tsx:279 — Prefer fake timers for the 2-second/20-second cadence checks; the real-time waits add several seconds and are scheduler-sensitive.

Strengths

  • The whitespace-only, same-marker, same-or-longer closing rule is directionally correct.
  • The hidden queue regression test targets the actual observable polling bug rather than relying only on call counts.
  • The HTML-comment helper has a narrow typed state contract and covers single-line, multiline, and unterminated comments.

Recommended Action

  1. Separate opener and closer grammars and isolate comment state from fenced content.
  2. Add the adversarial parser cases above before merge.
  3. Reopen this exact head under an independent author before an App approval is possible; the allyblockcast App cannot review its own PR.

@kkroo kkroo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the parser hardening and ReviewQueueCard polling cleanup. The ownership parser now ignores owning-looking labels inside list-nested fences, unterminated fences, and HTML comments, with regression coverage for both safe drops and visible-owner controls. The UI timer cleanup is covered by the hidden empty-queue polling test. No active review threads remain; review and security-review are green.

@kkroo
kkroo merged commit 57561d9 into sre/blo-20886-pr-review-wake-routing Aug 5, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant