feat: application auto rules#243
Conversation
Implement automated status categorization and knockout rules for job applications. This includes a new `application_rule` database schema, backend evaluation logic during submission, and UI updates to display automation status and triggering questions to recruiters.
- Create `ApplicationRulesBuilder` component for managing per-job status automation rules. - Add `useApplicationRules` composable to handle rule fetching, atomic updates, and triggering retroactive evaluation. - Implement server-side CRUD and `POST /run` endpoints for rule management. - Define shared `shared/application-rules.ts` as the single source of truth for rule evaluation logic, operator metadata, and types. - Add database migrations for `application_rule` table and `auto_rule` metadata on applications. - Add server-side rule engine to evaluate rules against application answers.
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughAdds configurable application automation rules with shared evaluation logic, database storage, authenticated management APIs, a job-level builder, retroactive execution, submission-time evaluation, and application-detail indicators for automatic decisions. ChangesApplication automation rules
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
server/api/jobs/[id]/rules/run.post.ts (1)
37-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffRetroactive run is unbounded and does redundant per-application work.
newAppsis fetched without a limit and each iteration callsapplyRulesToApplication, which re-loads the job's rules and question-type metadata every time (same data for every application). On a job with a largenewbacklog this becomes a long, synchronous request doing N×(several queries) work with no timeout.Consider loading the job's enabled rules + question types once and passing them in, and/or processing in bounded batches or a background job for large sets.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/jobs/`[id]/rules/run.post.ts around lines 37 - 62, Bound the retroactive run in the handler around newApps and applyRulesToApplication: load the job’s enabled rules and question-type metadata once, reuse them for every application, and process only a bounded batch or enqueue larger backlogs for background processing. Preserve the existing evaluated, matched, byAction, and logging behavior for the applications handled by the request, using the relevant rule-evaluation symbols rather than repeating per-application data-loading queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/api/jobs/`[id]/rules/index.put.ts:
- Around line 33-44: Update the insert payload in the transaction around
tx.insert(applicationRule).values so the mapped rules are explicitly typed as
RuleCondition[] before insertion. Preserve the existing field mapping and
displayOrder behavior while ensuring condition values conform to RuleCondition,
including null where supported by the database schema.
In `@server/utils/rules/applyRules.ts`:
- Around line 93-110: The guarded update in applyRulesToApplication must only
report a match when it changes a row. Capture the result of the update
identified by the application status condition, check its affected-row count,
and return null before logInfo and the existing return match when no row was
updated; preserve the current match path when the update succeeds.
In `@server/utils/schemas/applicationRule.ts`:
- Around line 8-18: Update conditionSchema validation used by bulkRulesSchema to
require a non-null, non-empty value for every comparison-based operator,
rejecting empty strings and arrays before rules are saved. Keep value optional
only for presence operators such as is_answered, is_provided, and is_true, while
preserving valid scalar and option-list values for operators that require them.
---
Nitpick comments:
In `@server/api/jobs/`[id]/rules/run.post.ts:
- Around line 37-62: Bound the retroactive run in the handler around newApps and
applyRulesToApplication: load the job’s enabled rules and question-type metadata
once, reuse them for every application, and process only a bounded batch or
enqueue larger backlogs for background processing. Preserve the existing
evaluated, matched, byAction, and logging behavior for the applications handled
by the request, using the relevant rule-evaluation symbols rather than repeating
per-application data-loading queries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c9b2d212-5887-497e-8af5-b6397d58a734
📒 Files selected for processing (20)
app/components/AppTopBar.vueapp/components/ApplicationBuilder.vueapp/components/ApplicationRulesBuilder.vueapp/composables/useApplicationRules.tsapp/pages/dashboard/jobs/[id]/index.vueapp/pages/dashboard/jobs/[id]/rules.vueserver/api/jobs/[id]/rules/index.get.tsserver/api/jobs/[id]/rules/index.put.tsserver/api/jobs/[id]/rules/run.post.tsserver/api/public/jobs/[slug]/apply.post.tsserver/database/migrations/0051_marvelous_molly_hayes.sqlserver/database/migrations/0052_application_auto_rule.sqlserver/database/migrations/meta/0051_snapshot.jsonserver/database/migrations/meta/0052_snapshot.jsonserver/database/migrations/meta/_journal.jsonserver/database/schema/app.tsserver/utils/rules/applyRules.tsserver/utils/schemas/applicationRule.tsshared/application-rules.tstests/unit/application-rules.test.ts
| return tx.insert(applicationRule) | ||
| .values(body.rules.map((r, index) => ({ | ||
| organizationId: orgId, | ||
| jobId, | ||
| name: r.name, | ||
| matchType: r.matchType, | ||
| action: r.action, | ||
| enabled: r.enabled, | ||
| conditions: r.conditions, | ||
| displayOrder: index, | ||
| }))) | ||
| .returning() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the zod schema and the RuleCondition/enum contracts driving the insert.
fd -t f 'applicationRule.ts' server/utils/schemas --exec cat -n {}
echo '--- RuleCondition / enums ---'
fd -t f 'application-rules.ts' shared --exec sed -n '1,120p' {}
echo '--- column $type ---'
rg -nP -C2 "conditions:\s*jsonb" server/database/schema/app.tsRepository: reqcore-inc/reqcore
Length of output: 7489
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- target file outline/sections ---'
ast-grep outline 'server/api/jobs/[id]/rules/index.put.ts' --view expanded || true
sed -n '1,120p' 'server/api/jobs/[id]/rules/index.put.ts'
echo '--- nearby zod/type casting in route ---'
rg -n "bulkRulesSchema|zod|applicationRule|insert\\(\\)\\.values|for await|async" 'server/api/jobs/[id]/rules/index.put.ts'
echo '--- package versions mentioning drizzle / zod ---'
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock; do
[ -f "$f" ] && { echo "### $f"; rg -n "drizzle-orm|zod" "$f" | head -40; }
doneRepository: reqcore-inc/reqcore
Length of output: 4210
Align the zod rule condition type with the DB schema. bulkRulesSchema infers conditionSchema.value as number | undefined, which conflicts with RuleCondition.value allowing null; pass a typed RuleCondition[] payload into tx.insert().values(...) instead of relying on the inferred zod shape.
🧰 Tools
🪛 GitHub Actions: PR Validation / 0_Build, typecheck, and test.txt
[error] 34-34: TypeScript typecheck failed (nuxi typecheck). TS2769: No overload matches this call.
🪛 GitHub Actions: PR Validation / Build, typecheck, and test
[error] 34-34: TypeScript typecheck failed: TS2769: No overload matches this call.
🪛 GitHub Check: Build, typecheck, and test
[failure] 34-34:
No overload matches this call.
[failure] 34-34:
No overload matches this call.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/api/jobs/`[id]/rules/index.put.ts around lines 33 - 44, Update the
insert payload in the transaction around tx.insert(applicationRule).values so
the mapped rules are explicitly typed as RuleCondition[] before insertion.
Preserve the existing field mapping and displayOrder behavior while ensuring
condition values conform to RuleCondition, including null where supported by the
database schema.
Sources: Linters/SAST tools, Pipeline failures
| const conditionSchema = z.object({ | ||
| questionId: z.string().min(1, 'Pick a question'), | ||
| operator: z.enum(ALL_OPERATORS as [string, ...string[]]), | ||
| // Comparison value — string, number, or list of option labels. Omitted for | ||
| // presence operators (is_answered, is_provided, is_true, …). | ||
| value: z.union([ | ||
| z.string().max(1000), | ||
| z.number(), | ||
| z.array(z.string().max(500)).max(50), | ||
| z.null(), | ||
| ]).optional(), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate file and search related symbols"
git ls-files | rg 'server/utils/schemas/applicationRule\.ts|applicationRule' || true
wc -l server/utils/schemas/applicationRule.ts
echo
echo "Outline"
ast-grep outline server/utils/schemas/applicationRule.ts || true
echo
echo "Relevant schema/evaluator source"
cat -n server/utils/schemas/applicationRule.ts | sed -n '1,260p'Repository: reqcore-inc/reqcore
Length of output: 1937
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Find applicationRules operator definitions and evaluator"
git ls-files | rg 'application[ _-]?rules|applicationRule' || true
echo
echo "Search ALL_OPERATORS and evaluateCondition"
rg -n "ALL_OPERATORS|evaluateCondition|contains|not_equals|is_not_one_of|is_answered|is_provided|is_true|value:" .
echo
echo "Candidate files with operator definitions"
for f in $(rg -l "ALL_OPERATORS" .); do
echo "--- $f"
wc -l "$f"
sed -n '1,260p' "$f"
done
echo
echo "Candidate files with evaluateCondition"
for f in $(rg -l "function evaluateCondition|const evaluateCondition|export .*evaluateCondition|evaluateCondition\\(" .); do
echo "--- $f"
wc -l "$f"
sed -n '1,360p' "$f"
doneRepository: reqcore-inc/reqcore
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Shared application-rules evaluator lines 150-220"
sed -n '150,220p' shared/application-rules.ts | cat -n
echo
echo "Unit tests around bad/missing values"
sed -n '1,120p' tests/unit/application-rules.test.ts | cat -n
echo
echo "Behavioral probe of missing/empty comparator values"
node - <<'JS'
const operatorCases = [
[
{
title: 'contains with undefined equals missing question', response: 'foo', value: undefined
},
{
title: 'not_equals with undefined counts unanswered as not-equal', response: undefined, value: undefined
},
{
title: 'is_not_one_of with empty set matches every response', response: 'A', value: []
},
{
title: 'is_one_of with empty set never matches', response: 'A', value: []
},
{
title: 'equals with same value should match', response: 'Yes ', value: 'Yes'
},
{
title: 'not_contains with undefined always true', response: undefined, value: undefined
}
]
]
function toArray(v) {
if (Array.isArray(v)) return v.map(String)
if (v === null || v === undefined || v === '') return []
return [String(v)]
}
function isEmpty(v) {
if (v === null || v === undefined) return true
if (typeof v === 'string') return v.trim() === ''
if (Array.isArray(v)) return v.length === 0
return false
}
function eqText(a, b) {
return String(a).trim().toLowerCase() === String(b).trim().toLowerCase()
}
function evaluateCondition(condition) {
const { operator, value } = condition
const response = condition.response
switch (operator) {
case 'equals': return !isEmpty(response) && eqText(response, value)
case 'not_equals': return isEmpty(response) || !eqText(response, value)
case 'contains': return String(response ?? '').toLowerCase().includes(String(value ?? '').toLowerCase())
case 'not_contains': return !String(response ?? '').toLowerCase().includes(String(value ?? '').toLowerCase())
case 'is_one_of':
return toArray(value).map(s => s.toLowerCase()).includes(String(response ?? '').toLowerCase())
case 'is_not_one_of':
return !toArray(value).map(s => s.toLowerCase()).includes(String(response ?? '').toLowerCase())
default: return false
}
}
for (const c of operatorCases[0]) {
console.log(`${c.title} => ${evaluateCondition(c)}`)
}
JS
echo
echo "Zod refs for schema exports and bulk validation path"
rg -n "bulkRulesSchema|ruleSchema|conditionSchema|saveApplication|appRule" app shared server/api tests -g '!node_modules' | head -200Repository: reqcore-inc/reqcore
Length of output: 10937
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
# Read-only behavioral probe equivalent to the evaluator branches for the reported conditions.
response = "Any text response"
missing_response = None
empty_response = ""
response_list = ["A", "B"]
missing_value = None
empty_array = []
def isEmpty(v):
if v is None or v is missing_response:
return True
if isinstance(v, str):
return v.strip() == ""
if isinstance(v, list):
return len(v) == 0
return False
def toArray(v):
if isinstance(v, list):
return [str(item) for item in v]
if v is None or v is missing_response or isinstance(v, str) and v == "":
return []
return [str(v)]
print("contains(undefined value, nonempty response):", str(response).lower().find(str(missing_value).lower()) != -1)
print("not_contains(undefined value, missing response):", not str(matched:=missing_response) and str(response)=str(response)=str(response)=str(response)=str(response)=str(response)=str(response)=str(response)=str(response)=str(response)=str(response)
PYRepository: reqcore-inc/reqcore
Length of output: 577
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Put route"
cat -n server/api/jobs/[id]/rules/index.put.ts | sed -n '1,40p'
echo
echo "Deterministic probe of missing/empty comparator values in evaluator branches"
node - <<'JS'
function isEmpty(v) {
if (v === null || v === undefined) return true
if (typeof v === 'string') return v.trim() === ''
if (Array.isArray(v)) return v.length === 0
return false
}
function eqText(a, b) {
return String(a).trim().toLowerCase() === String(b).trim().toLowerCase()
}
function toArray(v) {
if (Array.isArray(v)) return v.map(String)
if (v === null || v === undefined || v === '') return []
return [String(v)]
}
const cases = [
{ op: 'contains', value: undefined, response: 'Any response text', expected: true },
{ op: 'not_contains', value: undefined, response: undefined, expected: true },
{ op: 'not_equals', value: undefined, response: undefined, expected: true },
{ op: 'is_not_one_of', value: [], response: 'A', expected: true },
{ op: 'includes_any', value: [], response: ['A'], expected: false },
{ op: 'includes_all', value: [], response: ['A'], expected: false },
{ op: 'includes_none', value: [], response: ['A'], expected: true },
{ op: 'contains', value: [], response: 'A', expected: true },
]
for (const c of cases) {
const { op, value, response } = c
let result
if (op === 'contains') result = String(response ?? '').toLowerCase().includes(String(value ?? '').toLowerCase())
else if (op === 'not_contains') result = !String(response ?? '').toLowerCase().includes(String(value ?? '').toLowerCase())
else if (op === 'not_equals') result = isEmpty(response) || !eqText(response, value)
else if (op === 'is_one_of') {
const set = toArray(value).map(String)
result = set.map(s => s.toLowerCase()).includes(String(response ?? '').toLowerCase())
} else if (op === 'is_not_one_of') {
const set = toArray(value).map(String)
result = !set.map(s => s.toLowerCase()).includes(String(response ?? '').toLowerCase())
} else if (op === 'includes_any') {
result = toArray(response).map(String).map(s => s.toLowerCase()).some(p => toArray(value).map(String).map(v => v.toLowerCase()).includes(p))
} else if (op === 'includes_all') {
result = toArray(value).map(String).map(v => v.toLowerCase()).length > 0 && toArray(value).map(String).map(v => v.toLowerCase()).every(w => toArray(response).map(String).map(p => p.toLowerCase()).includes(w))
} else if (op === 'includes_none') result = !toArray(value).map(String).map(v => v.toLowerCase()).some(w => toArray(response).map(String).map(p => p.toLowerCase()).includes(w))
console.log(`${op} ${String(value)} vs ${String(response)} => ${result}, expected=${c.expected}`)
}
JSRepository: reqcore-inc/reqcore
Length of output: 2369
Require values where conditions need them.
bulkRulesSchema accepts omitted/null/empty values for comparison-based operators, and the evaluator turns missing values into broad matches (contains, not_contains, not_equals) or empty set behavior (is_not_one_of). Reject required values by operator and reject empty strings/arrays or null for those comparison conditions before saving rules.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/utils/schemas/applicationRule.ts` around lines 8 - 18, Update
conditionSchema validation used by bulkRulesSchema to require a non-null,
non-empty value for every comparison-based operator, rejecting empty strings and
arrays before rules are saved. Keep value optional only for presence operators
such as is_answered, is_provided, and is_true, while preserving valid scalar and
option-list values for operators that require them.
Import inArray for future use and cast rule operator schema to use RuleOperator type for better type safety.
- Allow null `actorId` in `activity_log` to represent system actions - Enable left joins on `activity_log` for timeline views to show system entries - Optimize rule application to process batches with bounded concurrency - Record audit trails for auto-categorized status changes
|
🚅 Deployed to the reqcore-pr-243 environment in applirank
|
Summary
Type of change
Validation
DCO
Signed-off-by) viagit commit -sSummary by CodeRabbit
New Features
Bug Fixes