Skip to content

[No QA] Migrate ProposalPolice to Responses + Conversations API - #97199

Merged
chuckdries merged 58 commits into
mainfrom
Rory-ProposalPoliceResponsesConversations
Aug 19, 2026
Merged

[No QA] Migrate ProposalPolice to Responses + Conversations API#97199
chuckdries merged 58 commits into
mainfrom
Rory-ProposalPoliceResponsesConversations

Conversation

@roryabraham

@roryabraham roryabraham commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

ProposalPolice currently runs on the OpenAI Assistants API: the system prompt is configured manually in the OpenAI dashboard (not checked into code), and duplicate-proposal detection makes one Assistants thread/run per prior proposal on the issue (each with up to 90s of polling).

Assistants are being deprecated in favor of the Responses API. They will be shut off on 2026-08-26, so we're up against a real deadline soon here.

This migrates ProposalPolice to:

  • The Responses API (OpenAIUtils.promptResponses) on gpt-5.6-luna (replacing the Assistant's GPT-4o) for the comment-intent and edit-check calls — no more thread/run polling.
  • A persistent, per-issue OpenAI Conversation for duplicate-check, so the loop of one Assistants call per prior proposal becomes a single Responses call against the issue's Conversation history. The Conversation ID is tracked via a hidden marker in a bot comment on the issue.
  • A checked-in, composed system prompt (prompts/proposalPolice/*) split into small, focused instruction fragments per call type (comment-intent / edit-check / duplicate-check), replacing the single dashboard-configured prompt blob — each call only gets the instructions it actually needs.
  • JSON-schema structured outputs for each call's response shape, replacing manual "respond with JSON" prompt instructions.

Behaviour changes beyond the migration

The system prompt configured in the OpenAI dashboard is stale against contributingGuides/PROPOSAL_TEMPLATE.md: it still requires two sections that were removed in ed87b140dc6 and 0ced4a8d38c. On main that means the bot nags contributors whose proposals correctly follow the current template. The checked-in templateDefinition.ts matches the real template, which fixes that.

The created-comment path is reworked. The case-sensitive Proposal keyword gate is gone, because it meant an entire class of comment never reached any check at all — a bid for the job that never uses the word, like this one, returned at that gate before anything ran. Instead:

  1. Expensify employees are filtered out in the workflow. author_association isn't sufficient (it only reports MEMBER for publicly visible org members), so this reads Expensify/expensify-expensify team membership, which needs a read:org token. A check that errors skips the run rather than treating the author as an outside contributor.
  2. Template conformance is checked deterministically in code (isProposal). It's a definition, not a judgment, and an LLM checking it can drift from the template — which is exactly the main bug above.
  3. Only comments that fail it get an LLM call, classifying intent as SPAM (a claim on the job with no technical content of its own), GENUINE_ATTEMPT (real technical content, wrong format), or NOT_AN_ATTEMPT (feedback, retests, questions, discussion).
  4. SPAM is collapsed via GraphQL minimizeComment, which leaves the text intact and is reversible — deliberately not the duplicate path's body overwrite. GENUINE_ATTEMPT stays visible. Both are pointed at the template.

The model reports intent and the code decides the action, so the enforcement policy stays in one reviewable, testable place.

Call volume. Measured over the 7 days to 2026-08-15 across the 61 open Help Wanted issues: 303 non-bot comments, of which 98 already follow the template and so cost nothing. That leaves ~29 intent calls/day (~880/month), an upper bound since Expensify employees are filtered out before the action runs and aren't excluded from that count. Duplicate-check volume is unchanged at ~14/day. The instruction prefix is cached via promptCacheKey, so the marginal cost of each call is the comment body plus a one-word response.

Two edit-check gates are new rather than ported, both fixing live main bugs: edits that didn't change the body are skipped (on main these reached the model as Previous comment content: undefined, which reads as a full rewrite and could produce a spurious banner), and edits are skipped unless either side of the edit is a proposal (so a heavily reworded discussion comment that merely mentions "Proposal" isn't bannered).

Duplicate detection never matches a proposal against its own author's. The old prompt asked for this but neither implementation honoured it, and the Conversation makes it more likely to bite: main compared pairwise and stopped at the first hit, whereas the model now sees every prior proposal at once and reports a single highest score, making a contributor's own earlier draft a real candidate.

No tests previously existed for ProposalPolice or OpenAIUtils; this PR adds coverage for all of the new logic (OpenAIUtilsTest, ProposalPoliceConversationTest, ProposalPoliceInputTest, ProposalPoliceSchemaTest, ProposalUtilsTest, proposalPoliceCommentTest).

Fixed Issues

$ #72725

Tests

Outside of the automated tests we added, this will be live-tested.

  • Verify that no errors appear in the JS console

Offline tests

None — this is a GitHub Actions bot with no client-side/offline behavior.

QA Steps

None — see [No QA] in the PR title. This is a backend-only GitHub Actions change with no user-facing app behavior.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

n/a

Replaces the deprecated Assistants thread/run polling helpers
(promptAssistant, parseAssistantResponse) with createConversation,
addConversationItems, and a generic parseJSONResponse<T>, so callers
can use the Responses API with persistent Conversations instead.
ProposalUtils holds the proposal/bot-detection helpers previously
private to proposalPoliceComment.ts, so the new conversation-tracking
utils can share them without a reverse import.

GithubUtils.pinIssue is a best-effort GraphQL pin (the REST API has no
equivalent) used to surface ProposalPolice's per-issue duplicate-check
tracking comment; failures are swallowed since pinning is a convenience,
not something the caller depends on.
Splits the dashboard-configured Assistant prompt into small, focused
fragments (template definition, template/edit examples, decision tree,
bot actions, duplicate detection) with per-call-type assemblers, so
each Responses API call only gets the instructions it actually needs
instead of the whole prompt every time. Also drops a stale "re-state
the problem" section from the examples that no longer matches the
proposal template.

Adds JSON-schema definitions and type guards for the three response
shapes (template-check, edit-check, duplicate-check), replacing the
"respond with JSON" instructions previously baked into the prompt text.
Pure, independently-testable helpers for the duplicate-check
Conversation flow: finding a tracked Conversation ID from a hidden
marker on a bot-authored comment, building the tracking comment body,
seeding items for prior proposals, and chunking items to OpenAI's
20-per-call Conversation limit.
Swaps promptAssistant for promptResponses on gpt-5.6-luna (replacing
the Assistant's GPT-4o) for template-check and edit-check, and
replaces the per-prior-proposal Assistants loop with a single
Responses call against a persistent per-issue Conversation for
duplicate-check.

Removes the now-unused PROPOSAL_POLICE_ASSISTANT_ID input/secret, and
exports `run` (guarded behind a JEST_WORKER_ID check on the
auto-invocation) so it can be unit tested directly.

Fixes #72725
Covers OpenAIUtils (promptResponses, createConversation,
addConversationItems, parseJSONResponse), the ProposalPoliceConversation
tracking helpers, and proposalPoliceComment's run() end-to-end
(NO_ACTION, ACTION_REQUIRED, ACTION_EDIT, duplicate withdrawal,
bot-author skip, and the Conversation create/reuse flow).

No tests previously existed for either of these.
Regenerates every action's ncc bundle via npm run gh-actions-build.
Most of these only pick up the new GithubUtils.pinIssue method (a
shared lib bundled into every action); proposalPoliceComment's bundle
reflects its full migration off the Assistants API.
- Require the model's action to be ACTION_HIDE_DUPLICATE, not just a
  high similarity score, before withdrawing a proposal as a duplicate
  (guards against the two fields disagreeing).
- Skip the duplicate-check Responses call entirely when an issue has
  no prior proposals to compare against, instead of always spending an
  API call on a comparison that can't find anything.
- Add a concurrency group (scoped by issue number) to the workflow so
  two comments posted close together on the same issue can't each
  create their own tracking Conversation.
- Add tests for both behavior changes.
Round-2 branch-reviewer catch: the duplicate-check Responses call was
the only mechanism appending items to a Conversation (via its
`conversation` param's auto-append behavior), so skipping that call
for an issue's first proposal (introduced in the previous commit) also
skipped ever recording it - permanently hiding it from every future
duplicate check on that issue. Now the proposal is recorded directly
via addConversationItems when the call is skipped.

Adds a regression test that runs the action twice in sequence (first
proposal, then a near-duplicate second one) to prove the first
proposal is actually comparable.
- Post the tracking comment (and pin) immediately after creating a
  Conversation, before sending any remaining seed-item batches, so a
  failure mid-seeding can't leave the Conversation permanently
  untracked and fragment duplicate-detection history across issues
  with a large pre-existing proposal backlog.
- Validate the model-reported duplicateCommentId actually matches a
  real proposal comment before using it to build the withdrawal
  notice's link.
- Escape angle brackets in untrusted comment/proposal text before
  interpolating it into our XML-style wrapper tags, so a comment
  containing a literal closing tag can't be mistaken by the model for
  the end of our own wrapper.
- Switch the auto-invocation guard from checking JEST_WORKER_ID to the
  require.main === module pattern already used by every other action
  in .github/actions/javascript/*.
- Add tests: multi-batch seeding (>20 prior proposals) with an
  assertion on tracking-comment-before-remaining-seed ordering, and
  escaping/tagging coverage for all four prompt input builders.
- Exclude the new proposal's own comment ID from the duplicate-check
  originalProposal lookup, guarding against a model self-match
  hallucination linking the withdrawal notice to itself.
- Add tests for GithubUtils.pinIssue (correct GraphQL call, and that
  errors are swallowed rather than thrown), the one piece of new logic
  from this migration that didn't yet have coverage.
Round-5 branch-reviewer note: no test asserted the actual model value
passed to promptResponses, so a future accidental edit to
PROPOSAL_POLICE_MODEL wouldn't be caught by the suite. Exports the
constant and asserts it's used for all three call types (duplicate,
template, and edit check).

(The model ID itself, gpt-5.6-luna, was independently confirmed to be
a real, GA OpenAI model as of 2026-07-09 before this migration began.)
Pinning is a scarce, repo-wide resource (max 3 pinned issues for the
entire repo), used by maintainers for things like contributing guides
or roadmap items. ProposalPolice would have attempted to consume one
of those slots on the first proposal of every "Help Wanted" issue,
which could fire constantly on an active repo and conflict with real
pins - for zero functional benefit, since the hidden marker text in
the tracking comment (not pin status) is the actual mechanism used to
find a tracked Conversation.

Reverts the corresponding +1 eslint-seatbelt allowance bump for
GithubUtilsTest.ts back to its original value now that the pinIssue
test (and its one unsafe-type-assertion) is gone too.
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 5 files with indirect coverage changes

…ctions one

getIsBotAuthor duplicated GitHub-specific bot detection that already
exists (and is better) in the GitHub-Actions repo, so port it in as
isBotUser instead of reimplementing it under ProposalUtils.
getIsProposal, getDuplicateCheckWithdrawMessage, and
getDuplicateCheckNoticeMessage aren't property getters, so name them
like the boolean/builder functions they are (isProposal,
buildDuplicateCheckWithdrawMessage, buildDuplicateCheckNoticeMessage),
consistent with the other build* helpers in this flow.
It took no arguments and always returned the same string, so a
function call was unnecessary indirection.
Testing hasPriorProposals instead of conversationID left conversationID
as string | undefined at the addConversationItems call, failing
typecheck. The two conditions are equivalent here, since
hasPriorProposals is assigned !!conversationID on the line above.
The prompts stated the same rules several times over:

- decisionTree restated NEW_COMMENT_ACTIONS step for step, and its
  "is it actually a proposal?" step was already covered by the
  identification examples, so delete it outright.
- duplicateDetection stated its one scoring rule four times, across
  Instructions / IMPORTANT / EXAMPLES / Summary. State it once.
- templateCheckExamples repeated an identical example verbatim across
  its validation and identification sections. Merge them.

Also define the ROOT CAUSE / SOLUTION / ALTERNATIVES shorthand once in
templateDefinition, rather than respelling the full section headings in
every file that refers to them.
The model was made to echo fixed message text back to us verbatim,
including a long template URL and {user} / {updated_timestamp}
placeholders that JS then substituted. Any drift in reproducing that
text silently changed what contributors saw.

Drop `message` from both schemas: the model now returns only an action,
and messages.ts owns every byte we post. The "already flagged" guard
also stops matching a hardcoded string and uses the exported
SUBSTANTIVE_EDIT_MESSAGE_PREFIX, so the message and the guard that
detects it can no longer drift apart.
The cutoff lived in two places that disagreed. The prompt told the model
to set ACTION_HIDE_DUPLICATE only at similarity >= 90, and JS gated on
that action *and* its own >= 85 check, so a proposal scoring 87 came
back as NO_ACTION and was never withdrawn. Lowering the threshold to 85
in c24faa1 therefore had no effect at all.

Drop `action` from the duplicate-check schema. The model reports only a
similarity score, and DUPLICATE_SIMILARITY_THRESHOLD decides, so the
cutoff is tunable in one place without the model having to reproduce it.
ACTION_HIDE_DUPLICATE has no remaining callers, so remove it too.
PR #98402 moved every test whose import graph reaches @actions/* or
@octokit/* out of Jest, so that those dependencies can be upgraded to
their pure-ESM versions. These suites import the action, so they reach
both and belong there too — left in Jest they would break on that
upgrade.

Three things did not survive the move as written:

- The suite relied on Jest's globally-faked timers to skip
  retryWithBackoff's delay, and bun:test has no global fake timers and no
  async timer advance to install. It now waits out the real second, which
  exercises the actual backoff.
- jest.mock('@scripts/utils/OpenAIUtils') auto-mocked the class. Its
  replacement is a real class rather than a mock constructor, because
  several tests call clearAllMocks partway through and that would strip a
  mock constructor's implementation.
- resetAllMocks does not clear these module-level mocks' queued
  mockResolvedValueOnce values, so one test's leftovers were being
  consumed by the next. They are reset by name instead.
It has no @actions import of its own, but leaving one ProposalPolice
suite behind in Jest splits them across two runners for no reason.
@melvin-bot

melvin-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

npm has a package.json file and a package-lock.json file. It seems you updated one without the other, which is usually a sign of a mistake. If you are updating a package make sure that you update the version in package.json then run npm install

@roryabraham

Copy link
Copy Markdown
Contributor Author

Measured it and added the figures to the description.

Over the 7 days to 2026-08-15, across the 61 open Help Wanted issues: 303 non-bot comments, 98 of which already follow the template and cost nothing. So ~29 intent calls/day, ~880/month. That's an upper bound — Expensify employees are filtered out before the action runs and aren't excluded from the count. Duplicate-check volume is unchanged at ~14/day.

@roryabraham

Copy link
Copy Markdown
Contributor Author

All ten are done in d504199376a, plus the test move in 4461eac011f.

One correction on the framing: these weren't lost. None of them were posted in round 1 — I checked every comment on the PR, and each of these appears for the first time in the comment above. Not a complaint, just so you're not left thinking a reply went missing.

  • <new_proposal> vs <proposal>. Both are <proposal> now, and the prompt names the last message as the one under review. buildDuplicateCheckSeedItem delegates to buildDuplicateCheckInput so the two can't drift.
  • Verdicts accumulating. Pruned after each duplicate check via findVerdictItemIDs. findConversationItemIDForComment also filters to user messages now, so a verdict naming a comment ID can't be mistaken for the proposal itself — there's a test for exactly that.
  • No direct tests for the delete path. OpenAIUtils.test.ts covers listConversationItems (including pagination) and deleteConversationItem, asserting the argument order, since swapping it would delete nothing and fail silently.
  • Banner guard includes vs startsWith. Now startsWith, and the strip runs against the same trimmed body the guard checked.
  • Attribute collisions. escapeForXMLWrapper escapes quotes as well as angle brackets.
  • Null-user double negative. buildSeedItems and findTrackedConversationID read the field the same way.
  • as Partial<...> casts and the dead ?? -1. Guards narrow with in instead; the coalesce is gone.
  • ProposalUtils.ts. Renamed to isProposal.ts, test alongside.
  • Duplicated test helpers. VALID_PROPOSAL_BODY and makeComment live in tests/tooling/proposalPoliceFixtures.ts. html_url and node_id moved onto ProposalComment, which also let the cast in mockComments go.
  • Instruction isolation. ProposalPoliceInstructions.test.ts asserts each call gets the role and template and none of the other two calls' rules.

@roryabraham

Copy link
Copy Markdown
Contributor Author

On N1

There's no N1 on the PR. The findings go N2, N3, N4, N5 — it's referenced only in your approval comment, so I think it's sitting in an unsubmitted pending review that only you can see.

Going from your one-line description ("the team-slug 404 ambiguity in the workflow, which silently makes every employee spam-eligible if the slug or token scope ever changes"): the ambiguity is real, and I checked rather than assumed —

$ gh api /orgs/Expensify/teams/this-team-does-not-exist/memberships/roryabraham
gh: Not Found (HTTP 404)          # broken slug
$ gh api /orgs/Expensify/teams/expensify-expensify/memberships/octocat
gh: Not Found (HTTP 404)          # genuine non-member

So the three-way check separates errors from 404s, but can't separate those two 404s.

I'm not fixing it. It takes a deliberate config change to trigger — a team rename or the token losing read:org — and the blast radius is small: employees on Help Wanted issues are giving direction, so their comments classify NOT_AN_ATTEMPT and nothing happens to them. Closing it means resolving the team on every comment, which is a permanent API call per comment to guard against a config change that would break plenty of other things too.

Worth naming: "fail loudly" isn't available here as an alternative. A broken slug doesn't produce an error to be loud about, it produces a successful 404 meaning "not a member". Detecting it is the extra call.

If you think that's the wrong call, submit N1 and I'll take another look — you may have a failure mode in mind that I'm not seeing from the summary line.

The merge brought a new lockfile that I hadn't installed, so the bundles
were built against 2.0.197 and were missing three validate-code actions
that CI's clean install has.
@roryabraham
roryabraham requested a review from ikevin127 August 18, 2026 21:39
bun:test's jest.fn() has no type without one, so every .mock.calls read
and every argument these were handed came through as any. Jest's
jest.mocked() had been supplying that for free.
// Bypass the real JSON-schema validators here; OpenAIUtils.test.ts already covers parseJSONResponse itself.
mockParseJSONResponse.mockImplementation((text) => JSON.parse(text));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 The verdict-pruning loop has no integration test

ProposalPoliceConversation.test.ts covers findVerdictItemIDs in isolation, and OpenAIUtils.test.ts covers deleteConversationItem.

But every action test's mock returns user messages only:

function conversationMessage(itemID: string, commentID: number): ConversationItem {
    return {id: itemID, type: 'message', role: 'user', status: 'completed', content: [...]};
}

So findVerdictItemIDs returns [] in all 27 tests and the loop body in run() never executes. The pieces are tested, the wiring is not. Cheapest fix is inside the test that already exercises the delete path:

mockListConversationItems.mockResolvedValue([
    conversationMessage('item_42', 42),
    conversationMessage('item_99', 99),
    {id: 'item_verdict', type: 'message', role: 'assistant', status: 'completed', content: [{type: 'output_text', text: '{"similarity":95,"duplicateCommentID":42}'}]},
]);

then assert both item_verdict and item_99 are deleted. The existing toHaveBeenCalledTimes(1) becomes 2, which is the assertion that would have caught a prune loop that silently does nothing.

@ikevin127

Copy link
Copy Markdown
Contributor

🟢 Worth one check during live testing

findVerdictItemIDs matches type === 'message' && role === 'assistant'. If gpt-5.6-luna persists reasoning items into the Conversation, they are not type: 'message' and would accumulate past the prune. After the first real duplicate check on a live issue, list that Conversation's items once and confirm only <proposal> user messages remain.

If reasoning items do show up, widening the filter is a one-line change.

@ikevin127 ikevin127 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 LGTM

Two new notes above ☝️ , neither blocking

All 14 items from rounds 1 and 2 are resolved, N1 was my error (never posted, your counter-argument on the merits is sound). The only real blocker left is an ESLint failure inherited from the main merge, in src/ files this PR never touches.

chuckdries
chuckdries previously approved these changes Aug 19, 2026
Five violations in four src/ files are unbaselined on main and block any
PR that busts the ESLint cache. This one does, because it adds an evals
project to config/eslint/eslint.config.mjs and the cache key hashes that
directory, so it re-lints the whole repo instead of only changed files.

The files are byte-identical to main. Three of the casts became
redundant in f2d5991, which gave getCardFeedWithDomainID overloads
that already return the asserted type, and the fourth is a
react-navigation deprecation.

Seatbelt already covered these files for no-unsafe-type-assertion, so
recording the new rule also tightens the old counts against the same
casts rather than adding debt. Auto-tighten will drop the entries once
the owners fix them properly.
These recorded five violations that #98943 has since fixed at the
source, so the entries describe casts and a deprecated call that no
longer exist.
@roryabraham
roryabraham requested a review from chuckdries August 19, 2026 05:09
@chuckdries
chuckdries merged commit c7f3807 into main Aug 19, 2026
46 of 47 checks passed
@chuckdries
chuckdries deleted the Rory-ProposalPoliceResponsesConversations branch August 19, 2026 05:22
@github-actions

Copy link
Copy Markdown
Contributor

🚧 chuckdries has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/chuckdries in version: 9.4.57-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/Beamanator in version: 9.4.57-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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.

4 participants