You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit #102675
Coming from here, PolicyUtils.ts has gotten close to it's max size, which leads to preventable issues where we go over the limit and then suddenly the build is broken, blocking deploys and other people's PRs.
changed the title [-][Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]on Sep 30, 2026
Sizes are the current function bodies, not a promise about the formatted result. Scattered helpers are called out so a move is not "cut this line range" when the functions are not contiguous.
New file
What moves
Where it is now
~Size
Imports
types.ts
Exported types MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess, plus types used by more than one module (PolicyWithTaxRates, AccountingConnectionName, WorkspaceDetails, ApprovalWorkflowContext, ApprovalWorkflowRuleMatch, TravelStep)
72-111
~40
nothing
policyType.ts
Archived / pending-delete checks, paid vs Submit vs group vs Control vs Collect, canPolicyAccessFeature, isSubmitterApproveBlockedOnSubmitWorkspace
Active-policy filters, shouldShowPolicy, owned / group / chat-enabled policies, admin and employee workspaces, plan type, deletion guard, sort helpers, and the three invoice helpers (canSendInvoiceFromWorkspace, canSubmitPerDiemExpenseFromWorkspace, canSendInvoice)
Re-exports only, including the existing alias getAllTaxRatesNamesAndKeys as getAllTaxRates and export type {MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess}
3784-4039
~300
the modules above
index.ts at ~300 lines of re-exports is the public API. That is the point: callers and jest.mock('@libs/PolicyUtils') stay on the barrel.
Internal-only helpers stay private to their module. They are not in the export block today: the feature-permission maps, isControlPolicyOnlyRole, hasPolicyFeaturePermission, shouldShowPolicyErrorFields, getNumericValue, getDefaultDistanceRate, cloneCustomUnitWithNewIDs, the parent-tag regexes, isSubmitPolicyByType, isPolicyEditor, isControlOnAdvancedApprovalMode, getFirstRuleApprover and the approval-filter matchers, getAccountingConnectionNames, isAccountingConnectionName, the per-integration is*VendorMatchingActive checks that are not already exported, getActiveVendorMatchingVendors, and hasUnsupportedIntegration.
Suggested PR order
One PR that touches all 4039 lines will conflict with every open policy PR. Split it. Each step leaves index.ts re-exporting the moved names, so no importer changes.
tags.ts. Leaf, ~350 lines. This alone drops index.ts under 4000 and clears the lint failure. getRuleApprovers (still in the old file) imports getTagApproverRule from ./tags, not the other way around.
vendors.ts. Largest leaf, ~490 lines. The mention of getSageIntacctVendors inside it is a comment, not a call, so it does not need sageIntacct.ts yet.
policyType.ts, then permissions.ts.policyType.ts is a leaf. Extract it first in this PR, then move permissions.ts so it imports ./policyType. Do not extract permissions.ts while isControlPolicy / isArchivedPolicy / isSubmitPolicy still live in index.ts.
connections.ts, netsuite.ts, sageIntacct.ts, xero.ts. All leaves. ~400 lines together, or one PR per integration if that reviews more easily.
customUnits.ts and tax.ts.tax.ts imports getDistanceRateCustomUnit from ./customUnits.
rules.ts, features.ts, workflows.ts, errors.ts. These sit on policyType and connections, which already exist.
approvals.ts and payments.ts. Riskiest call graph (getSubmitToEmail, rule matching). Do it only after workflows, permissions, and tags are real modules.
workspaces.ts (including the three invoice helpers), members.ts (including bank-account sharing), domains.ts, travel.ts, navigation.ts. This empties the old file. index.ts becomes the barrel.
⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.
Please re-state the problem that we are trying to solve in this issue.
src/libs/PolicyUtils.ts has grown close to its maximum size, putting it at immediate risk of violating the ESLint 4,000-line ceiling and causing CI build failures that block deploys and subsequent PRs.
What is the root cause of that problem?
PolicyUtils.ts currently aggregates helper functions across diverse domains (tag manipulation, permission evaluation, connection utilities, vendor matching, custom units, and workflows) in a single monolithic file without submodule decomposition.
What changes do you think we should make in order to solve the problem?
Execute Step 1 of the maintainer's architectural roadmap established by @nkdengineer:
Extract tags.ts into src/libs/PolicyUtils/tags.ts:
Extract the ~350 lines of self-contained tag helpers:
Tag lists and sorting: getTagLists, getTagList, getTagListName, getTagListByOrderWeight, getTagListLabel
Tag validation and properties: getTagNamesFromTagsLists, getTagApproverRule, getTagGLCode, hasDependentTags, hasEnabledTags, getCountOfEnabledTagsOfList, hasEnabledOptions
Private tag helpers: getCleanedTagName, parent-tag regex matchers.
Note that tags.ts is a pure leaf module requiring zero unextracted policy dependencies, ensuring clean separation.
Preserve Public API & Re-exports:
In PolicyUtils.ts (or the PolicyUtils/index.ts barrel), re-export all extracted tag functions and types so that existing callers, consumers across the app, and jest.mock('@libs/PolicyUtils') remain 100% backward compatible without breaking changes or monorepo-wide import updates.
In PolicyUtils.ts, import getTagApproverRule from ./PolicyUtils/tags (or ./tags) for getRuleApprovers.
Validation & Verification:
Verify that PolicyUtils.ts drops well below the 4,000-line threshold (~3,500 lines), permanently resolving the ESLint limit danger for this stage.
Run the complete test suite (npm test -- PolicyUtilsTest) and ESLint checks (npm run lint) to ensure zero regressions and 100% green test passes.
What alternative solutions did you consider?
Attempting to refactor all 4,000 lines into 17 submodules in a single massive PR. This was ruled out because a single monolithic refactor PR would inevitably produce merge conflicts with every other in-flight PR touching policy code. Starting with tags.ts as a pure leaf is the safest, conflict-free, incremental strategy.
src/libs/PolicyUtils.ts is still one file. A sibling tags.ts cannot sit next to it until the module is a directory.
Move src/libs/PolicyUtils.ts to src/libs/PolicyUtils/index.ts.
Add src/libs/PolicyUtils/tags.ts.
Leave every existing import ... from '@libs/PolicyUtils' and from './PolicyUtils' as it is. Both resolve to index.ts.
index.ts keeps every current export, including getAllTaxRatesNamesAndKeys as getAllTaxRates. Callers and jest.mock('@libs/PolicyUtils') stay on that barrel. Nothing outside this folder should import @libs/PolicyUtils/tags.
What moves into tags.ts
Copy these functions unchanged. They only call each other, CONST, isEmptyObject, Str.htmlDecode, and getTagArrayFromName from @libs/TransactionUtils.
Keep LITERAL_PARENT_TAGS_FILTER and ESCAPED_CHARACTER private in tags.ts. They are not exported today.
Leave the block between the first two tag sections in index.ts: hasCustomCategories, isMaxExpenseAmountSet, and hasConfiguredRules (1306-1398). Those belong to rules.ts in a later PR.
Start tags.ts with a short file-header comment describing tag-list lookup, parent-tag matching, and GL codes. Export the 27 functions above. index.ts imports them from ./tags and keeps their names in the existing export { ... } block.
The one edge back into index.ts
getRuleApprovers (line 2016) stays in index.ts and calls getTagApproverRule. After the move, that call comes from the ./tags import. tags.ts does not import index.ts, so there is no cycle and no import/no-cycle disable.
index.ts should land around 3450 lines, under the 4000-line max-lines cap.
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.13-6 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-26. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
changed the title [-][Due for payment 2026-10-26] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-27] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]on Oct 20, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.14-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
changed the title [-][Due for payment 2026-10-27] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-28] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]on Oct 21, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.15-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
@nkdengineer I'm willing to increase the payout but not nearly by that much. Largely because this was a refactor PR that primarily was just moving code from one file to another. Yes there was testing involved but it was clear from the beginning how much work this was going to be and asking for an 8.5x increase in pay is a lot. How about $500 total?
@blimpich Thanks for being open to raising it. I understand the concern about the jump size. Would you consider $750 total ($250 per PR across the 3 PRs)?
The code changes were mostly moving functions between files, as you said. Most of the time went into testing.
I think $500 is the max I would pay out for this. I appreciate the work but I would encourage you to seek a higher payout earlier in the process rather than at the end if it's going to be that big of an increase 👍
changed the title [-][Due for payment 2026-10-28] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]on Oct 22, 2026
Activity
melvin-bot commentedon Sep 30, 2026
Current assignee @nkdengineer is eligible for the External assigner, not assigning anyone new.
[-][Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]melvin-bot commentedon Sep 30, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105327754295937031
nkdengineer commentedon Sep 30, 2026
Split plan
Sizes are the current function bodies, not a promise about the formatted result. Scattered helpers are called out so a move is not "cut this line range" when the functions are not contiguous.
types.tsMemberEmailsToAccountIDs,PolicyFeature,PolicyFeatureAccess, plus types used by more than one module (PolicyWithTaxRates,AccountingConnectionName,WorkspaceDetails,ApprovalWorkflowContext,ApprovalWorkflowRuleMatch,TravelStep)policyType.tscanPolicyAccessFeature,isSubmitterApproveBlockedOnSubmitWorkspacetypespermissions.tsisPolicyAdmin,getPolicyRole, editor / auditor / user / employee), feature-permission bundles,canMember*,canEditWorkspaceSettings,isPolicyApprover,getPolicyApproverLoginspolicyTypetags.tsgetTagApproverRulerules.tshasCustomCategories,isMaxExpenseAmountSet,hasConfiguredRules,getRulesDocumentSourceURLcustomUnits.tsgetCustomUnitsForDuplication,cloneCustomUnitWithNewIDs)policyTypeconnections.tsisPreferredExporter, Uber receipt-partner errorvendors.tsnetsuite.tssageIntacct.tsxero.tshasPolicyWithXeroConnectiontax.tsisTaxTrackingEnabled, tax-rate maps,canDisableOrDeleteTaxRate,getCurrentTaxID,resolveCurrentTaxCode,isPolicyTaxEnabled,isTaxCodeCustomizedcustomUnitsfeatures.tsisPolicyFeatureEnabledand the feature checks it fans out to (arePolicyRulesEnabled, invoice fields, attendee tracking, time tracking, MCP)policyType,connectionsworkflows.tsisSubmitAndClose, dynamic external workflowconnectionserrors.tsareSettingsInErrorFields,settingsPendingActionpermissions,connectionspayments.tsPAYER_ROLES,canRolePay,isPolicyPayer, reimbursement choice, payments enabled, auto-pay availabilitypermissionsapprovals.tsgetWorkflowApprovalsUnavailableworkflows,policyType,permissions,tagsworkspaces.tsshouldShowPolicy, owned / group / chat-enabled policies, admin and employee workspaces, plan type, deletion guard, sort helpers, and the three invoice helpers (canSendInvoiceFromWorkspace,canSubmitPerDiemExpenseFromWorkspace,canSendInvoice)policyType,permissions,customUnits,featuresmembers.tsisDeletedPolicyEmployee, and bank-account share recipientspermissions,workspacesdomains.tsmemberstravel.tsgetTravelSteppolicyType,workspaces,domainsnavigation.tstryNavigateToSubmitWorkspaceUpgrade,tryNavigateToControlPolicyUpgrade,goBackFromInvalidPolicy,goBackWhenEnableFeature,navigateToExpensifyCardPagepolicyTypeindex.tsgetAllTaxRatesNamesAndKeys as getAllTaxRatesandexport type {MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess}index.tsat ~300 lines of re-exports is the public API. That is the point: callers andjest.mock('@libs/PolicyUtils')stay on the barrel.Internal-only helpers stay private to their module. They are not in the export block today: the feature-permission maps,
isControlPolicyOnlyRole,hasPolicyFeaturePermission,shouldShowPolicyErrorFields,getNumericValue,getDefaultDistanceRate,cloneCustomUnitWithNewIDs, the parent-tag regexes,isSubmitPolicyByType,isPolicyEditor,isControlOnAdvancedApprovalMode,getFirstRuleApproverand the approval-filter matchers,getAccountingConnectionNames,isAccountingConnectionName, the per-integrationis*VendorMatchingActivechecks that are not already exported,getActiveVendorMatchingVendors, andhasUnsupportedIntegration.Suggested PR order
One PR that touches all 4039 lines will conflict with every open policy PR. Split it. Each step leaves
index.tsre-exporting the moved names, so no importer changes.tags.ts. Leaf, ~350 lines. This alone dropsindex.tsunder 4000 and clears the lint failure.getRuleApprovers(still in the old file) importsgetTagApproverRulefrom./tags, not the other way around.vendors.ts. Largest leaf, ~490 lines. The mention ofgetSageIntacctVendorsinside it is a comment, not a call, so it does not needsageIntacct.tsyet.policyType.ts, thenpermissions.ts.policyType.tsis a leaf. Extract it first in this PR, then movepermissions.tsso it imports./policyType. Do not extractpermissions.tswhileisControlPolicy/isArchivedPolicy/isSubmitPolicystill live inindex.ts.connections.ts,netsuite.ts,sageIntacct.ts,xero.ts. All leaves. ~400 lines together, or one PR per integration if that reviews more easily.customUnits.tsandtax.ts.tax.tsimportsgetDistanceRateCustomUnitfrom./customUnits.rules.ts,features.ts,workflows.ts,errors.ts. These sit onpolicyTypeandconnections, which already exist.approvals.tsandpayments.ts. Riskiest call graph (getSubmitToEmail, rule matching). Do it only afterworkflows,permissions, andtagsare real modules.workspaces.ts(including the three invoice helpers),members.ts(including bank-account sharing),domains.ts,travel.ts,navigation.ts. This empties the old file.index.tsbecomes the barrel.nkdengineer commentedon Sep 30, 2026
@MelvinBot Here is the split-file plan for
PolicyUtils. Let's implement the first PR based onSuggested PR ordersection.MelvinBot commentedon Sep 30, 2026
⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.
Run details
camesenin commentedon Sep 30, 2026
Contributor details
Your Expensify account email: [email protected]
Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604
Proposal
Please re-state the problem that we are trying to solve in this issue.
src/libs/PolicyUtils.tshas grown close to its maximum size, putting it at immediate risk of violating the ESLint 4,000-line ceiling and causing CI build failures that block deploys and subsequent PRs.What is the root cause of that problem?
PolicyUtils.tscurrently aggregates helper functions across diverse domains (tag manipulation, permission evaluation, connection utilities, vendor matching, custom units, and workflows) in a single monolithic file without submodule decomposition.What changes do you think we should make in order to solve the problem?
Execute Step 1 of the maintainer's architectural roadmap established by @nkdengineer:
Extract
tags.tsintosrc/libs/PolicyUtils/tags.ts:getTagLists,getTagList,getTagListName,getTagListByOrderWeight,getTagListLabelgetTagNamesFromTagsLists,getTagApproverRule,getTagGLCode,hasDependentTags,hasEnabledTags,getCountOfEnabledTagsOfList,hasEnabledOptionsgetCleanedTagName, parent-tag regex matchers.tags.tsis a pure leaf module requiring zero unextracted policy dependencies, ensuring clean separation.Preserve Public API & Re-exports:
PolicyUtils.ts(or thePolicyUtils/index.tsbarrel), re-export all extracted tag functions and types so that existing callers, consumers across the app, andjest.mock('@libs/PolicyUtils')remain 100% backward compatible without breaking changes or monorepo-wide import updates.PolicyUtils.ts, importgetTagApproverRulefrom./PolicyUtils/tags(or./tags) forgetRuleApprovers.Validation & Verification:
PolicyUtils.tsdrops well below the 4,000-line threshold (~3,500 lines), permanently resolving the ESLint limit danger for this stage.npm test -- PolicyUtilsTest) and ESLint checks (npm run lint) to ensure zero regressions and 100% green test passes.What alternative solutions did you consider?
Attempting to refactor all 4,000 lines into 17 submodules in a single massive PR. This was ruled out because a single monolithic refactor PR would inevitably produce merge conflicts with every other in-flight PR touching policy code. Starting with
tags.tsas a pure leaf is the safest, conflict-free, incremental strategy.melvin-bot commentedon Sep 30, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
nkdengineer commentedon Oct 1, 2026
Turn the file into a folder
src/libs/PolicyUtils.tsis still one file. A siblingtags.tscannot sit next to it until the module is a directory.src/libs/PolicyUtils.tstosrc/libs/PolicyUtils/index.ts.src/libs/PolicyUtils/tags.ts.import ... from '@libs/PolicyUtils'andfrom './PolicyUtils'as it is. Both resolve toindex.ts.index.tskeeps every current export, includinggetAllTaxRatesNamesAndKeys as getAllTaxRates. Callers andjest.mock('@libs/PolicyUtils')stay on that barrel. Nothing outside this folder should import@libs/PolicyUtils/tags.What moves into
tags.tsCopy these functions unchanged. They only call each other,
CONST,isEmptyObject,Str.htmlDecode, andgetTagArrayFromNamefrom@libs/TransactionUtils.getSortedTagKeys,getTagListName,getTagLists,hasTags,matchesParentTagsFilter,matchesParentTagPath,findPolicyTagAtLevel,findPolicyTagEntryByParentFilter,isTagInPolicygetTagList,getTagListByOrderWeight,getTagNamesFromTagsLists,getCleanedTagName,getCommaSeparatedTagNameWithSanitizedColons,getLengthOfTag,getTagGLCode,getGLCodeFromPolicyTag,escapeTagName,isDefaultTagName,getCountOfEnabledTagsOfList,getCountOfRequiredTagLists,isMultiLevelTagshasDependentTags,hasIndependentTags,hasPerTagListRequired,getTagListLabelgetTagApproverRuleKeep
LITERAL_PARENT_TAGS_FILTERandESCAPED_CHARACTERprivate intags.ts. They are not exported today.Leave the block between the first two tag sections in
index.ts:hasCustomCategories,isMaxExpenseAmountSet, andhasConfiguredRules(1306-1398). Those belong torules.tsin a later PR.Start
tags.tswith a short file-header comment describing tag-list lookup, parent-tag matching, and GL codes. Export the 27 functions above.index.tsimports them from./tagsand keeps their names in the existingexport { ... }block.The one edge back into
index.tsgetRuleApprovers(line 2016) stays inindex.tsand callsgetTagApproverRule. After the move, that call comes from the./tagsimport.tags.tsdoes not importindex.ts, so there is no cycle and noimport/no-cycledisable.index.tsshould land around 3450 lines, under the 4000-linemax-linescap.83 remaining items
melvin-bot commentedon Oct 19, 2026
@nkdengineer
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.13-6 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-26. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
[-][Due for payment 2026-10-26] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-27] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]melvin-bot commentedon Oct 20, 2026
@nkdengineer
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.14-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-27. 🎊
[-][Due for payment 2026-10-27] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-28] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]melvin-bot commentedon Oct 21, 2026
@nkdengineer
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.15-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-28. 🎊
nkdengineer commentedon Oct 22, 2026
Not overdue. All PRs are complete.
nkdengineer commentedon Oct 22, 2026
blimpich commentedon Oct 22, 2026
@nkdengineer I'm willing to increase the payout but not nearly by that much. Largely because this was a refactor PR that primarily was just moving code from one file to another. Yes there was testing involved but it was clear from the beginning how much work this was going to be and asking for an 8.5x increase in pay is a lot. How about $500 total?
nkdengineer commentedon Oct 22, 2026
@blimpich Thanks for being open to raising it. I understand the concern about the jump size. Would you consider $750 total ($250 per PR across the 3 PRs)?
The code changes were mostly moving functions between files, as you said. Most of the time went into testing.
blimpich commentedon Oct 22, 2026
I think $500 is the max I would pay out for this. I appreciate the work but I would encourage you to seek a higher payout earlier in the process rather than at the end if it's going to be that big of an increase 👍
[-][Due for payment 2026-10-28] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-][+][Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+]melvin-bot commentedon Oct 22, 2026
Upwork job price has been updated to $500
nkdengineer commentedon Oct 22, 2026
Thanks. I noted that.