knowme-careerforge-skill 0.0.2 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/AGENT.md +276 -0
  2. package/ARCHITECTURE.md +216 -0
  3. package/README.md +34 -54
  4. package/README.zh-CN.md +35 -57
  5. package/SKILL.md +28 -137
  6. package/agents/claude/knowme-careerforge.md +2 -16
  7. package/agents/codex/knowme-careerforge.yaml +5 -7
  8. package/agents/cursor/knowme-careerforge.mdc +4 -9
  9. package/agents/opencode/skill.yaml +1 -1
  10. package/agents/windsurf/knowme-careerforge.rules +2 -12
  11. package/dist/cli/src/commands/init.js +9 -0
  12. package/dist/cli/src/index.js +40 -19
  13. package/dist/scripts/rendering/engine-loader.d.ts +2 -0
  14. package/dist/scripts/rendering/engine-loader.js +46 -0
  15. package/dist/scripts/rendering/render-pdf.d.ts +3 -8
  16. package/dist/scripts/rendering/render-pdf.js +7 -103
  17. package/dist/scripts/rendering/runtime.d.ts +9 -0
  18. package/dist/scripts/rendering/runtime.js +63 -0
  19. package/dist/scripts/validation/validate-ats.d.ts +2 -22
  20. package/dist/scripts/validation/validate-ats.js +6 -168
  21. package/dist/scripts/validation/validate-layout.d.ts +2 -23
  22. package/dist/scripts/validation/validate-layout.js +5 -159
  23. package/package.json +16 -4
  24. package/references/01-evidence-mining.md +5 -0
  25. package/references/02-career-goal.md +38 -0
  26. package/references/03-jd-analysis.md +44 -0
  27. package/references/04-template-selection.md +43 -0
  28. package/references/05-html-canvas-tokens.md +12 -0
  29. package/references/06-qa-and-rendering.md +14 -0
  30. package/references/07-artifact-contract.md +64 -0
  31. package/scripts/Agent.md +9 -136
  32. package/scripts/build/build-gallery.py +66 -56
  33. package/scripts/build/run-all-tests.py +1 -0
  34. package/scripts/build/sync-version.py +37 -10
  35. package/scripts/contracts/profile.py +101 -0
  36. package/scripts/evidence/extract-evidence.py +109 -62
  37. package/scripts/pipeline/forge.py +266 -136
  38. package/scripts/rendering/README.md +65 -0
  39. package/scripts/rendering/browser-engine.d.ts +15 -0
  40. package/scripts/rendering/browser-engine.js +366 -0
  41. package/scripts/rendering/browser_engine.py +39 -0
  42. package/scripts/rendering/engine-loader.ts +11 -0
  43. package/scripts/rendering/render-pdf.py +26 -151
  44. package/scripts/rendering/render-pdf.ts +7 -83
  45. package/scripts/template/instantiate-resume.py +266 -167
  46. package/scripts/template/search-template.py +367 -89
  47. package/scripts/validation/measure-dom.js +5 -0
  48. package/scripts/validation/validate-ats.ts +6 -174
  49. package/scripts/validation/validate-layout.ts +5 -159
  50. package/scripts/validation/validate-resume.py +23 -134
  51. package/skill.json +1 -1
  52. package/src/knowledge/index.json +390 -10
  53. package/src/knowledge/resume-schema.json +377 -71
  54. package/src/knowledge/templates.json +388 -8
  55. package/src/references/01-evidence-mining.md +5 -0
  56. package/src/references/02-career-goal.md +38 -0
  57. package/src/references/03-jd-analysis.md +44 -0
  58. package/src/references/04-template-selection.md +43 -0
  59. package/src/references/05-html-canvas-tokens.md +12 -0
  60. package/src/references/06-qa-and-rendering.md +14 -0
  61. package/src/references/07-artifact-contract.md +64 -0
  62. package/src/templates/Agent.md +11 -0
  63. package/src/templates/academic-research/README.md +25 -0
  64. package/src/templates/academic-research/canvas.html +21 -0
  65. package/src/templates/academic-research/metadata.json +38 -0
  66. package/src/templates/academic-research/sample-profile.json +86 -0
  67. package/src/templates/academic-research/style.css +213 -0
  68. package/src/templates/classic/README.md +9 -0
  69. package/src/templates/classic/canvas.html +21 -0
  70. package/src/templates/classic/metadata.json +3 -0
  71. package/src/templates/classic/sample-profile.json +86 -0
  72. package/src/templates/classic/style.css +35 -54
  73. package/src/templates/common/base.css +182 -0
  74. package/src/templates/common/canvas-bindings.css +41 -0
  75. package/src/templates/common/resume-contract.md +8 -0
  76. package/src/templates/compact-dense/README.md +25 -0
  77. package/src/templates/compact-dense/canvas.html +14 -0
  78. package/src/templates/compact-dense/metadata.json +38 -0
  79. package/src/templates/compact-dense/sample-profile.json +86 -0
  80. package/src/templates/compact-dense/style.css +218 -0
  81. package/src/templates/creative-tech/README.md +26 -0
  82. package/src/templates/creative-tech/canvas.html +15 -0
  83. package/src/templates/creative-tech/metadata.json +39 -0
  84. package/src/templates/creative-tech/sample-profile.json +86 -0
  85. package/src/templates/creative-tech/style.css +257 -0
  86. package/src/templates/data-analyst/README.md +26 -0
  87. package/src/templates/data-analyst/canvas.html +15 -0
  88. package/src/templates/data-analyst/metadata.json +39 -0
  89. package/src/templates/data-analyst/sample-profile.json +86 -0
  90. package/src/templates/data-analyst/style.css +285 -0
  91. package/src/templates/executive/README.md +9 -0
  92. package/src/templates/executive/canvas.html +15 -0
  93. package/src/templates/executive/sample-profile.json +86 -0
  94. package/src/templates/executive/style.css +83 -74
  95. package/src/templates/international-flow/README.md +26 -0
  96. package/src/templates/international-flow/canvas.html +21 -0
  97. package/src/templates/international-flow/metadata.json +37 -0
  98. package/src/templates/international-flow/sample-profile.json +86 -0
  99. package/src/templates/international-flow/style.css +208 -0
  100. package/src/templates/minimal/README.md +9 -0
  101. package/src/templates/minimal/canvas.html +21 -0
  102. package/src/templates/minimal/sample-profile.json +86 -0
  103. package/src/templates/minimal/style.css +54 -72
  104. package/src/templates/modern/README.md +9 -0
  105. package/src/templates/modern/canvas.html +15 -0
  106. package/src/templates/modern/sample-profile.json +86 -0
  107. package/src/templates/modern/style.css +78 -71
  108. package/src/templates/startup-generalist/README.md +25 -0
  109. package/src/templates/startup-generalist/canvas.html +21 -0
  110. package/src/templates/startup-generalist/metadata.json +37 -0
  111. package/src/templates/startup-generalist/sample-profile.json +86 -0
  112. package/src/templates/startup-generalist/style.css +237 -0
  113. package/src/references/ats.md +0 -17
  114. package/src/references/evidence-first.md +0 -22
  115. package/src/references/pdf.md +0 -40
  116. package/src/references/resume-writing.md +0 -25
  117. package/src/references/template-design.md +0 -57
  118. package/src/templates/classic/template.html +0 -133
  119. package/src/templates/executive/template.html +0 -101
  120. package/src/templates/minimal/template.html +0 -114
  121. package/src/templates/modern/template.html +0 -139
  122. package/src/workflows/analyze-job.md +0 -16
  123. package/src/workflows/define-goal.md +0 -16
  124. package/src/workflows/forge-resume.md +0 -25
  125. package/src/workflows/know-me.md +0 -21
  126. package/src/workflows/position.md +0 -19
  127. package/src/workflows/review.md +0 -31
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- KnowMe CareerForge — Template Search & Ranking Engine (BM25 + Multi-criteria scoring)
4
- 根据目标岗位、期望版式风格、目标页数与信息密度,加权检索最匹配的 HTML 简历模板。
3
+ KnowMe CareerForge — Template Search & Ranking Engine
4
+ Architecture: Deep Module with Pluggable Scorer Strategies (Weighted Rules + BM25 Text Search)
5
+
6
+ Design Contract:
7
+ - BaseTemplateScorer: Abstract strategy interface
8
+ - WeightedRuleScorer: Multi-criteria rule-based weighted scorer (Role 35%, Style 25%, ATS 20%, Page 10%, Density 10%)
9
+ - BM25TextScorer: Pure-Python BM25 keyword retrieval across template text documents
10
+ - HybridTemplateScorer: Weighted blend (70% Rule Score + 30% BM25 Text Score)
11
+ - search_templates(): Public facade interface
5
12
  """
6
13
 
7
- import sys, os, json, argparse
14
+ import sys, os, json, argparse, math, re
8
15
  from pathlib import Path
16
+ from dataclasses import dataclass, field
9
17
  from typing import List, Dict, Any, Optional
18
+ from abc import ABC, abstractmethod
19
+
10
20
 
11
21
  def get_project_root() -> Path:
12
22
  curr = Path(__file__).resolve().parent
@@ -16,6 +26,17 @@ def get_project_root() -> Path:
16
26
  curr = curr.parent
17
27
  return Path.cwd()
18
28
 
29
+
30
+ @dataclass
31
+ class SearchQuery:
32
+ role: str
33
+ style: Optional[str] = None
34
+ target_pages: int = 1
35
+ density: str = "balanced"
36
+ keywords: Optional[str] = None
37
+ engine: str = "hybrid" # "weighted" | "bm25" | "hybrid"
38
+
39
+
19
40
  def load_templates() -> List[Dict[str, Any]]:
20
41
  root_dir = get_project_root()
21
42
  base_dir = root_dir / "src" / "templates"
@@ -30,120 +51,377 @@ def load_templates() -> List[Dict[str, Any]]:
30
51
  if meta_file.exists():
31
52
  try:
32
53
  data = json.loads(meta_file.read_text(encoding="utf-8"))
33
- data["directory"] = f"src/templates/{t_dir.name}"
54
+ data["_directory"] = str(t_dir.relative_to(root_dir))
34
55
  templates.append(data)
35
56
  except Exception:
36
57
  pass
37
58
  return templates
38
59
 
39
- def calculate_match_score(template: Dict[str, Any], query_role: str, query_style: Optional[str] = None, target_pages: int = 1, density: str = "balanced") -> float:
40
- score = 0.0
60
+
61
+ # ==============================================================================
62
+ # 1. SCORER STRATEGY INTERFACE & IMPLEMENTATIONS
63
+ # ==============================================================================
64
+
65
+ class BaseTemplateScorer(ABC):
66
+ """Abstract interface for template scoring algorithms."""
41
67
 
42
- # 1. 岗位匹配度 (Role Match - 35%)
43
- role_score = 0.0
44
- query_lower = query_role.lower()
45
- supported = [r.lower() for r in template.get("supportedRoles", [])]
68
+ @abstractmethod
69
+ def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
70
+ """Return match score normalized between 0.0 and 100.0."""
71
+ pass
72
+
73
+
74
+ class WeightedRuleScorer(BaseTemplateScorer):
75
+ """
76
+ Rule-based weighted scoring engine:
77
+ - Role Match: 35%
78
+ - Style Match: 25%
79
+ - ATS Tier: 20%
80
+ - Page Fit: 10%
81
+ - Density Fit: 10%
82
+ """
46
83
 
47
- for r in supported:
48
- if r in query_lower or query_lower in r:
49
- role_score = 1.0
50
- break
51
-
52
- if role_score < 0.5:
53
- cat = template.get("roleCategory", "")
54
- if "ai" in query_lower or "algorithm" in query_lower or "agent" in query_lower:
55
- role_score = 0.95 if cat == "engineering-ai" else 0.40
56
- elif "product" in query_lower or "manager" in query_lower or "director" in query_lower:
57
- role_score = 0.95 if cat == "management-product" else 0.40
58
- elif "engineer" in query_lower or "architect" in query_lower:
59
- role_score = 0.85
60
- else:
61
- role_score = 0.60
62
-
63
- score += role_score * 0.35
64
-
65
- # 2. 风格匹配度 (Style Match - 25%)
66
- style_score = 0.70
67
- if query_style:
68
- q_style_lower = query_style.lower()
69
- t_style = template.get("style", "").lower()
70
- t_tone = template.get("visualStyle", {}).get("tone", "").lower()
84
+ def __init__(self, weights: Optional[Dict[str, float]] = None):
85
+ self.weights = weights or {
86
+ "role": 0.35,
87
+ "style": 0.25,
88
+ "ats": 0.20,
89
+ "page": 0.10,
90
+ "density": 0.10
91
+ }
92
+
93
+ def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
94
+ score = 0.0
95
+
96
+ # 1. Role Match
97
+ role_score = 0.0
98
+ query_lower = query.role.lower()
99
+ supported = [r.lower() for r in template.get("supportedRoles", [])]
71
100
 
72
- if q_style_lower in t_style or q_style_lower in t_tone or q_style_lower == template.get("id"):
73
- style_score = 1.0
74
- elif "minimal" in q_style_lower and "minimal" in t_style:
75
- style_score = 0.95
76
- elif "split" in q_style_lower and "split" in t_style:
77
- style_score = 0.95
78
- elif "modern" in q_style_lower and "modern" in t_tone:
79
- style_score = 0.95
101
+ for r in supported:
102
+ if r in query_lower or query_lower in r:
103
+ role_score = 1.0
104
+ break
105
+
106
+ if role_score < 0.5:
107
+ cat = template.get("roleCategory", "")
108
+ if any(k in query_lower for k in ["research", "scientist", "algorithm", "phd", "academic", "postdoc", "算法", "科研", "学者", "研究员"]) and "research" in cat:
109
+ role_score = 0.90
110
+ elif any(k in query_lower for k in ["remote", "international", "global", "overseas", "foreign", "外企", "海外", "跨国", "远程"]) and "international" in cat:
111
+ role_score = 0.90
112
+ elif any(k in query_lower for k in ["creative", "ui", "ux", "design", "interaction", "动效", "交互", "视觉"]) and "creative" in cat:
113
+ role_score = 0.90
114
+ elif any(k in query_lower for k in ["data", "analyst", "bi", "analytics", "metric", "数据", "商业智能", "数仓"]) and "data" in cat:
115
+ role_score = 0.90
116
+ elif any(k in query_lower for k in ["startup", "indie", "growth", "founder", "hacker", "初创", "增长", "独立开发"]) and "startup" in cat:
117
+ role_score = 0.90
118
+ elif any(k in query_lower for k in ["ai", "agent", "llm", "frontend", "backend", "fullstack", "devops", "engineer"]) and "engineering" in cat:
119
+ role_score = 0.85
120
+ elif any(k in query_lower for k in ["manager", "product", "lead", "director", "architect", "cto", "高管", "总监"]) and "management" in cat:
121
+ role_score = 0.85
122
+ elif any(k in query_lower for k in ["bank", "fintech", "state", "corp", "government"]) and "corporate" in cat:
123
+ role_score = 0.85
124
+ else:
125
+ role_score = 0.60
126
+ score += role_score * self.weights["role"]
127
+
128
+ # 2. Style Match
129
+ style_score = 0.70
130
+ if query.style:
131
+ q_style_lower = query.style.lower()
132
+ t_style = template.get("style", "").lower()
133
+ t_tone = template.get("visualStyle", {}).get("tone", "").lower()
134
+
135
+ if q_style_lower in t_style or q_style_lower in t_tone:
136
+ style_score = 1.0
137
+ elif any(k in t_style for k in q_style_lower.split("-")):
138
+ style_score = 0.85
139
+ else:
140
+ style_score = 0.50
141
+ score += style_score * self.weights["style"]
142
+
143
+ # 3. ATS Tier
144
+ ats_tier = template.get("atsScoreTier", "tier-1-optimal")
145
+ ats_score = 1.0 if "optimal" in ats_tier or "tier-1" in ats_tier else 0.85
146
+ score += ats_score * self.weights["ats"]
147
+
148
+ # 4. Page Fit
149
+ target_p = template.get("layout", {}).get("targetPages", 1)
150
+ max_p = template.get("layout", {}).get("maxPages", 2)
151
+ if query.target_pages == target_p:
152
+ page_score = 1.0
153
+ elif query.target_pages <= max_p:
154
+ page_score = 0.80
80
155
  else:
81
- style_score = 0.50
82
- score += style_score * 0.25
83
-
84
- # 3. ATS 兼容评级 (ATS Tier - 20%)
85
- ats_tier = template.get("atsScoreTier", "tier-1-optimal")
86
- ats_score = 1.0 if "optimal" in ats_tier or "tier-1" in ats_tier else 0.85
87
- score += ats_score * 0.20
88
-
89
- # 4. 页数适配度 (Page Fit - 10%)
90
- target_p = template.get("layout", {}).get("targetPages", 1)
91
- max_p = template.get("layout", {}).get("maxPages", 2)
92
- if target_pages == target_p:
93
- page_score = 1.0
94
- elif target_pages <= max_p:
95
- page_score = 0.80
96
- else:
97
- page_score = 0.50
98
- score += page_score * 0.10
156
+ page_score = 0.50
157
+ score += page_score * self.weights["page"]
158
+
159
+ # 5. Density Preference
160
+ t_density = template.get("layout", {}).get("density", "balanced")
161
+ density_score = 1.0 if query.density == t_density else 0.80
162
+ score += density_score * self.weights["density"]
163
+
164
+ return round(score * 100, 1)
165
+
166
+
167
+ class BM25TextScorer(BaseTemplateScorer):
168
+ """
169
+ Pure Python BM25 Text Scorer across template metadata documents.
170
+ Constructs a virtual document per template: (name, category, style, supportedRoles, tone).
171
+ """
172
+
173
+ def __init__(self, templates: Optional[List[Dict[str, Any]]] = None, k1: float = 1.5, b: float = 0.75):
174
+ self.k1 = k1
175
+ self.b = b
176
+ self.templates = templates or load_templates()
177
+ self.docs = [self._extract_doc(t) for t in self.templates]
178
+ self.avg_dl = sum(len(d) for d in self.docs) / max(len(self.docs), 1)
179
+ self.doc_freqs = self._calc_doc_freqs()
180
+
181
+ def _extract_doc(self, template: Dict[str, Any]) -> List[str]:
182
+ text_parts = [
183
+ template.get("name", ""),
184
+ template.get("style", ""),
185
+ template.get("roleCategory", ""),
186
+ template.get("visualStyle", {}).get("tone", ""),
187
+ " ".join(template.get("supportedRoles", []))
188
+ ]
189
+ combined = " ".join(text_parts).lower()
190
+ # Tokenize words & Chinese characters
191
+ tokens = re.findall(r'[a-zA-Z0-9_\-]+|[\u4e00-\u9fa5]', combined)
192
+ return tokens
193
+
194
+ def _calc_doc_freqs(self) -> Dict[str, int]:
195
+ freqs: Dict[str, int] = {}
196
+ for doc in self.docs:
197
+ for word in set(doc):
198
+ freqs[word] = freqs.get(word, 0) + 1
199
+ return freqs
200
+
201
+ def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
202
+ doc = self._extract_doc(template)
203
+ doc_len = len(doc)
204
+ if doc_len == 0:
205
+ return 50.0
206
+
207
+ query_text = f"{query.role} {query.style or ''} {query.keywords or ''}".lower()
208
+ q_tokens = re.findall(r'[a-zA-Z0-9_\-]+|[\u4e00-\u9fa5]', query_text)
209
+ if not q_tokens:
210
+ return 70.0
99
211
 
100
- # 5. 信息密度偏好 (Density - 10%)
101
- t_density = template.get("layout", {}).get("density", "balanced")
102
- density_score = 1.0 if density == t_density else 0.80
103
- score += density_score * 0.10
212
+ n_docs = len(self.docs)
213
+ score = 0.0
104
214
 
105
- return round(score * 100, 1)
215
+ for term in q_tokens:
216
+ df = self.doc_freqs.get(term, 0)
217
+ if df == 0:
218
+ continue
219
+ # Standard BM25 IDF with smoothing
220
+ idf = math.log(1 + (n_docs - df + 0.5) / (df + 0.5))
221
+ tf = doc.count(term)
222
+ numerator = tf * (self.k1 + 1)
223
+ denominator = tf + self.k1 * (1 - self.b + self.b * (doc_len / max(self.avg_dl, 1)))
224
+ score += idf * (numerator / max(denominator, 0.001))
106
225
 
107
- def search_templates(role: str, style: Optional[str] = None, target_pages: int = 1, density: str = "balanced") -> List[Dict[str, Any]]:
226
+ # Normalize score into roughly 50.0 ~ 100.0 range
227
+ normalized = 50.0 + min(score * 15.0, 50.0)
228
+ return round(normalized, 1)
229
+
230
+
231
+ class HybridTemplateScorer(BaseTemplateScorer):
232
+ """
233
+ Weighted combination of rule-based constraints and BM25 text relevance.
234
+ Default: 70% Weighted Rules + 30% BM25 Relevance.
235
+ """
236
+
237
+ def __init__(self, rule_weight: float = 0.70, bm25_weight: float = 0.30):
238
+ self.rule_scorer = WeightedRuleScorer()
239
+ self.bm25_scorer = BM25TextScorer()
240
+ self.rule_weight = rule_weight
241
+ self.bm25_weight = bm25_weight
242
+
243
+ def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
244
+ rule_score = self.rule_scorer.score(template, query)
245
+ bm25_score = self.bm25_scorer.score(template, query)
246
+ blended = (rule_score * self.rule_weight) + (bm25_score * self.bm25_weight)
247
+ return round(blended, 1)
248
+
249
+
250
+ PALETTE_PRESETS: Dict[str, Dict[str, Any]] = {
251
+ "--palette-tech-blue": {
252
+ "name": "经典科技蓝",
253
+ "token": "--palette-tech-blue",
254
+ "hex": "#2563eb",
255
+ "tone": "tech-blue",
256
+ "keywords": ["engineering", "frontend", "backend", "fullstack", "devops", "cloud", "software", "前端", "后端", "全栈"]
257
+ },
258
+ "--palette-deep-navy": {
259
+ "name": "深邃海航蓝",
260
+ "token": "--palette-deep-navy",
261
+ "hex": "#1e3a8a",
262
+ "tone": "oxford-navy",
263
+ "keywords": ["research", "academic", "algorithm", "phd", "scientist", "ai-agent", "llm", "cv", "nlp", "算法", "科研", "学者", "博士"]
264
+ },
265
+ "--palette-teal-modern": {
266
+ "name": "现代松石绿",
267
+ "token": "--palette-teal-modern",
268
+ "hex": "#0f766e",
269
+ "tone": "executive-formal",
270
+ "keywords": ["executive", "management", "cto", "director", "lead", "product", "architect", "高管", "总监", "架构师", "产品经理"]
271
+ },
272
+ "--palette-emerald-fresh": {
273
+ "name": "活力翡翠绿",
274
+ "token": "--palette-emerald-fresh",
275
+ "hex": "#059669",
276
+ "tone": "emerald-green",
277
+ "keywords": ["startup", "generalist", "indie", "growth", "founder", "hacker", "初创", "独立开发", "增长"]
278
+ },
279
+ "--palette-violet-creative": {
280
+ "name": "创意极光紫",
281
+ "token": "--palette-violet-creative",
282
+ "hex": "#7c3aed",
283
+ "tone": "tech-violet",
284
+ "keywords": ["creative", "ui", "ux", "design", "interaction", "threejs", "webgl", "motion", "动效", "视觉", "体验"]
285
+ },
286
+ "--palette-slate-minimal": {
287
+ "name": "极简碳素灰",
288
+ "token": "--palette-slate-minimal",
289
+ "hex": "#334155",
290
+ "tone": "neutral-slate",
291
+ "keywords": ["international", "remote", "global", "overseas", "minimal", "compact", "dense", "外企", "海外", "远程", "极简"]
292
+ }
293
+ }
294
+
295
+ def recommend_palette(role: str, template: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
296
+ """Recommend official palette preset based on role keywords and template style."""
297
+ role_lower = (role or "").lower()
298
+ template_tone = (template.get("visualStyle", {}).get("tone", "") if template else "").lower()
299
+ template_cat = (template.get("roleCategory", "") if template else "").lower()
300
+
301
+ # 1. Match against palette keywords
302
+ for token, p in PALETTE_PRESETS.items():
303
+ if any(kw in role_lower for kw in p["keywords"]):
304
+ return p
305
+
306
+ # 2. Match template visual tone
307
+ for token, p in PALETTE_PRESETS.items():
308
+ if p["tone"] in template_tone or template_tone in p["tone"]:
309
+ return p
310
+
311
+ # 3. Default fallback
312
+ return PALETTE_PRESETS["--palette-tech-blue"]
313
+
314
+ # ==============================================================================
315
+ # 2. PUBLIC FACADE INTERFACE (DEEP MODULE)
316
+ # ==============================================================================
317
+
318
+ def search_templates(
319
+ role: str,
320
+ style: Optional[str] = None,
321
+ target_pages: int = 1,
322
+ density: str = "balanced",
323
+ keywords: Optional[str] = None,
324
+ engine: str = "hybrid"
325
+ ) -> List[Dict[str, Any]]:
326
+ """
327
+ Search and rank HTML resume templates.
328
+
329
+ Args:
330
+ role: Target job title or direction (e.g., 'AI Agent Engineer', 'Tech Lead')
331
+ style: Preferred visual/layout style (e.g., 'single-column-minimal', 'two-column-split')
332
+ target_pages: Expected page count (1 or 2)
333
+ density: Information density preference ('high', 'balanced', 'spacious')
334
+ keywords: Optional JD keywords for text matching
335
+ engine: Scoring engine ('hybrid' | 'weighted' | 'bm25')
336
+
337
+ Returns:
338
+ List of matching templates sorted by matchScore descending.
339
+ """
340
+ query = SearchQuery(
341
+ role=role,
342
+ style=style,
343
+ target_pages=target_pages,
344
+ density=density,
345
+ keywords=keywords,
346
+ engine=engine
347
+ )
348
+
108
349
  templates = load_templates()
350
+ if not templates:
351
+ return []
352
+
353
+ # Select Scorer Strategy
354
+ scorer: BaseTemplateScorer
355
+ if engine == "bm25":
356
+ scorer = BM25TextScorer(templates)
357
+ elif engine == "weighted":
358
+ scorer = WeightedRuleScorer()
359
+ else:
360
+ scorer = HybridTemplateScorer()
361
+
109
362
  results = []
110
-
111
363
  for t in templates:
112
- s = calculate_match_score(t, role, style, target_pages, density)
113
- results.append({
114
- "score": s,
115
- "template": t
116
- })
364
+ t_copy = dict(t)
365
+ t_copy["recommendedPalette"] = recommend_palette(role, t)
366
+ score_val = scorer.score(t, query)
367
+ t_copy["matchScore"] = score_val
368
+ t_copy["score"] = score_val
369
+ t_copy["template"] = t
370
+ results.append(t_copy)
117
371
 
118
- results.sort(key=lambda x: x["score"], reverse=True)
372
+ results.sort(key=lambda x: x.get("matchScore", 0.0), reverse=True)
119
373
  return results
120
-
121
374
  def main():
122
375
  parser = argparse.ArgumentParser(description="KnowMe CareerForge — Template Search Engine")
123
- parser.add_argument("role", nargs="?", default="AI Agent Engineer", help="Target role title")
124
- parser.add_argument("--style", "-s", default=None, help="Desired style (minimal, modern, executive, classic)")
125
- parser.add_argument("--target-pages", "-p", type=int, default=1, help="Expected target pages (1 or 2)")
126
- parser.add_argument("--density", "-d", default="balanced", help="Information density preference (high, balanced, normal)")
127
- parser.add_argument("--json", action="store_true", help="Output results in raw JSON")
128
-
376
+ parser.add_argument("role", nargs="?", default=None, help="Target role or job title (e.g. 'AI Agent Engineer')")
377
+ parser.add_argument("--role", "-r", dest="named_role", default=None, help="Target role (named option)")
378
+ parser.add_argument("--style", "-s", default=None, help="Style preference (minimal, modern, executive, classic)")
379
+ parser.add_argument("--target-pages", "-p", type=int, default=1, help="Target pages (1 or 2)")
380
+ parser.add_argument("--density", "-d", default="balanced", choices=["high", "balanced", "spacious"], help="Density")
381
+ parser.add_argument("--keywords", "-k", default=None, help="Additional JD keywords")
382
+ parser.add_argument("--engine", "-e", default="hybrid", choices=["hybrid", "weighted", "bm25"], help="Scoring engine")
383
+ parser.add_argument("--json", action="store_true", help="Output raw JSON array")
384
+
129
385
  args = parser.parse_args()
130
- results = search_templates(args.role, style=args.style, target_pages=args.target_pages, density=args.density)
386
+ target_role = args.named_role or args.role
387
+ if not target_role:
388
+ parser.error("the following arguments are required: role (or --role / -r)")
131
389
 
390
+ results = search_templates(
391
+ role=target_role,
392
+ style=args.style,
393
+ target_pages=args.target_pages,
394
+ density=args.density,
395
+ keywords=args.keywords,
396
+ engine=args.engine
397
+ )
398
+
132
399
  if args.json:
133
400
  print(json.dumps(results, ensure_ascii=False, indent=2))
134
401
  return
135
-
402
+
136
403
  print("=" * 80)
137
- print(f" KnowMe CareerForge — Template Recommendations for '{args.role}'")
404
+ print(f" KnowMe CareerForge — Template Search Engine Results")
405
+ print(f" Query Role : {target_role}")
406
+ print(f" Style Filter : {args.style or 'Any'}")
407
+ print(f" Engine : {args.engine.upper()}")
408
+ print(f" Matches : {len(results)} template(s) found")
138
409
  print("=" * 80)
410
+
139
411
  for idx, r in enumerate(results, 1):
140
- t = r["template"]
141
- print(f"{idx}. [Score: {r['score']}] {t['name']} (ID: {t['id']})")
142
- print(f" Category: {t['roleCategory']} | Style: {t['style']} | ATS Tier: {t['atsScoreTier']}")
143
- print(f" Target Pages: {t['layout']['targetPages']} (Max: {t['layout']['maxPages']}) | Density: {t['layout']['density']}")
144
- print(f" Tone: {t['visualStyle']['tone']} | Accent: {t['visualStyle']['accentColor']}")
145
- print(f" Directory: {t['directory']}")
412
+ custom_tokens = r.get("customizableTokens", [])
413
+ tokens_preview = ", ".join(custom_tokens[:4]) + ("..." if len(custom_tokens) > 4 else "")
414
+ print(f"\n{idx}. [{r.get('matchScore')} pts] {r.get('name')} (ID: {r.get('id')})")
415
+ print(f" Category: {r.get('roleCategory')} | Style: {r.get('style')} | ATS Tier: {r.get('atsScoreTier')}")
416
+ print(f" Target Pages: {r.get('layout', {}).get('targetPages', 1)} | Density: {r.get('layout', {}).get('density')}")
417
+ print(f" Directory: {r.get('_directory')}")
418
+ pal = r.get("recommendedPalette")
419
+ if pal:
420
+ print(f" Palette : {pal.get('name')} ({pal.get('hex')}) [Token: {pal.get('token')}]")
421
+ if tokens_preview:
422
+ print(f" Tokens : {tokens_preview}")
146
423
  print("-" * 80)
147
424
 
425
+
148
426
  if __name__ == "__main__":
149
427
  main()
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const engine = require('../rendering/browser-engine.js');
4
+ module.exports = engine;
5
+ if (require.main === module) engine.cli();