feat: initialize project structure with SQLite database, crawler framework, and LLM-based JD analysis integration

This commit is contained in:
2026-08-12 01:13:53 -07:00
commit 51b9468737
27 changed files with 3422 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package filter
import "context"
type FilterResult struct {
Keep bool
Score float64
Reason string
}
type JobFilter interface {
Name() string
Process(ctx context.Context, title, rawContent string) (FilterResult, error)
}
type FilterPipeline struct {
filters []JobFilter
}
func NewPipeline(filters ...JobFilter) *FilterPipeline {
return &FilterPipeline{filters: filters}
}
func (p *FilterPipeline) Execute(ctx context.Context, title, rawContent string) (bool, float64) {
var totalScore float64
for _, f := range p.filters {
res, err := f.Process(ctx, title, rawContent)
if err != nil || !res.Keep {
return false, 0
}
totalScore += res.Score
}
return true, totalScore
}
+58
View File
@@ -0,0 +1,58 @@
package filter
import (
"context"
"testing"
)
func TestHardKeywordFilter(t *testing.T) {
ctx := context.Background()
fk := NewHardKeywordFilter([]string{"DevOps", "No Sponsorship"})
// Should fail due to DevOps
res1, err := fk.Process(ctx, "Senior DevOps Engineer", "We build pipelines.")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res1.Keep {
t.Errorf("expected Keep=false for DevOps, got true")
}
// Should pass
res2, err := fk.Process(ctx, "Senior Backend Engineer", "We build scalable APIs in Go.")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !res2.Keep {
t.Errorf("expected Keep=true for Backend Engineer, got false")
}
}
func TestFilterPipeline(t *testing.T) {
ctx := context.Background()
hardFilter := NewHardKeywordFilter([]string{"Unpaid", "No Sponsorship"})
scoringFilter := NewScoringFilter([]KeywordWeight{
{Keyword: "Go", Weight: 30.0},
{Keyword: "Backend", Weight: 20.0},
})
pipeline := NewPipeline(hardFilter, scoringFilter)
// Test job 1: Unpaid job -> rejected by hard filter
keep1, score1 := pipeline.Execute(ctx, "Backend Intern", "Unpaid position working with Go.")
if keep1 {
t.Errorf("expected job 1 to be filtered out, got keep=true")
}
if score1 != 0 {
t.Errorf("expected score 0 for rejected job, got %f", score1)
}
// Test job 2: Qualified job -> passes both filters and accumulates score
keep2, score2 := pipeline.Execute(ctx, "Senior Go Backend Engineer", "Building high throughput services.")
if !keep2 {
t.Errorf("expected job 2 to pass filter pipeline, got keep=false")
}
if score2 != 50.0 {
t.Errorf("expected score 50.0 (Go + Backend), got %f", score2)
}
}
+48
View File
@@ -0,0 +1,48 @@
package filter
import (
"context"
"fmt"
"strings"
)
type HardKeywordFilter struct {
Blacklist []string
}
func NewHardKeywordFilter(blacklist []string) *HardKeywordFilter {
if len(blacklist) == 0 {
blacklist = []string{
"no sponsorship",
"us citizen only",
"unpaid",
"volunteer",
}
}
return &HardKeywordFilter{Blacklist: blacklist}
}
func (f *HardKeywordFilter) Name() string {
return "HardKeywordFilter"
}
func (f *HardKeywordFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
combined := strings.ToLower(title + " " + rawContent)
for _, kw := range f.Blacklist {
term := strings.ToLower(strings.TrimSpace(kw))
if term != "" && strings.Contains(combined, term) {
return FilterResult{
Keep: false,
Score: 0,
Reason: fmt.Sprintf("Contains blacklisted keyword: %q", kw),
}, nil
}
}
return FilterResult{
Keep: true,
Score: 0,
Reason: "Passed hard keyword check",
}, nil
}
+59
View File
@@ -0,0 +1,59 @@
package filter
import (
"context"
"fmt"
"strings"
)
type KeywordWeight struct {
Keyword string
Weight float64
}
type ScoringFilter struct {
Weights []KeywordWeight
}
func NewScoringFilter(weights []KeywordWeight) *ScoringFilter {
if len(weights) == 0 {
weights = []KeywordWeight{
{Keyword: "go", Weight: 25.0},
{Keyword: "golang", Weight: 25.0},
{Keyword: "backend", Weight: 15.0},
{Keyword: "distributed systems", Weight: 20.0},
{Keyword: "microservices", Weight: 15.0},
{Keyword: "sqlite", Weight: 10.0},
{Keyword: "postgres", Weight: 10.0},
{Keyword: "api", Weight: 10.0},
{Keyword: "full stack", Weight: 10.0},
{Keyword: "rest", Weight: 5.0},
{Keyword: "grpc", Weight: 15.0},
}
}
return &ScoringFilter{Weights: weights}
}
func (s *ScoringFilter) Name() string {
return "ScoringFilter"
}
func (s *ScoringFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
combined := strings.ToLower(title + " " + rawContent)
var totalScore float64
var matched []string
for _, kw := range s.Weights {
term := strings.ToLower(strings.TrimSpace(kw.Keyword))
if term != "" && strings.Contains(combined, term) {
totalScore += kw.Weight
matched = append(matched, kw.Keyword)
}
}
return FilterResult{
Keep: true,
Score: totalScore,
Reason: fmt.Sprintf("Matched %d keywords: %v", len(matched), matched),
}, nil
}