knowme-careerforge-skill 0.0.1 → 0.0.2

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 (47) hide show
  1. package/README.md +6 -9
  2. package/README.zh-CN.md +6 -9
  3. package/SKILL.md +95 -116
  4. package/agents/claude/knowme-careerforge.md +9 -8
  5. package/agents/codex/knowme-careerforge.yaml +15 -6
  6. package/agents/cursor/knowme-careerforge.mdc +7 -6
  7. package/agents/opencode/skill.yaml +1 -1
  8. package/agents/windsurf/knowme-careerforge.rules +11 -8
  9. package/dist/cli/src/commands/init.d.ts +1 -1
  10. package/dist/cli/src/commands/init.js +37 -10
  11. package/dist/cli/src/index.js +65 -22
  12. package/dist/scripts/render-pdf.d.ts +0 -9
  13. package/dist/scripts/render-pdf.js +5 -68
  14. package/dist/scripts/rendering/render-pdf.d.ts +11 -0
  15. package/dist/scripts/rendering/render-pdf.js +109 -0
  16. package/dist/scripts/validate-ats.d.ts +0 -21
  17. package/dist/scripts/validate-ats.js +5 -132
  18. package/dist/scripts/validate-layout.d.ts +0 -22
  19. package/dist/scripts/validate-layout.js +5 -124
  20. package/dist/scripts/validation/validate-ats.d.ts +23 -0
  21. package/dist/scripts/validation/validate-ats.js +173 -0
  22. package/dist/scripts/validation/validate-layout.d.ts +24 -0
  23. package/dist/scripts/validation/validate-layout.js +164 -0
  24. package/package.json +15 -12
  25. package/scripts/Agent.md +138 -0
  26. package/scripts/{build-gallery.py → build/build-gallery.py} +128 -99
  27. package/scripts/{build-knowledge.py → build/build-knowledge.py} +9 -1
  28. package/scripts/{release.py → build/release.py} +20 -16
  29. package/scripts/{run-all-tests.py → build/run-all-tests.py} +9 -1
  30. package/scripts/build/sync-version.py +107 -0
  31. package/scripts/{analyze-jd.py → evidence/analyze-jd.py} +34 -45
  32. package/scripts/evidence/extract-evidence.py +348 -0
  33. package/scripts/pipeline/forge.py +142 -0
  34. package/scripts/rendering/render-pdf.py +162 -0
  35. package/scripts/{render-pdf.ts → rendering/render-pdf.ts} +3 -3
  36. package/scripts/template/instantiate-resume.py +178 -0
  37. package/scripts/{search-template.py → template/search-template.py} +44 -42
  38. package/scripts/{validate-ats.ts → validation/validate-ats.ts} +39 -35
  39. package/scripts/{validate-layout.ts → validation/validate-layout.ts} +43 -42
  40. package/scripts/{validate-resume.py → validation/validate-resume.py} +31 -22
  41. package/skill.json +1 -1
  42. package/src/knowledge/index.json +1 -1
  43. package/src/knowledge/resume-schema.json +112 -0
  44. package/src/workflows/forge-resume.md +2 -2
  45. package/src/workflows/review.md +3 -3
  46. package/scripts/instantiate-resume.py +0 -83
  47. package/scripts/sync-version.py +0 -57
@@ -12,8 +12,8 @@ interface AtsValidationResult {
12
12
  status: 'PASS' | 'WARN' | 'FAIL';
13
13
  candidateName: string | null;
14
14
  detectedContacts: {
15
- phone: string | null;
16
15
  email: string | null;
16
+ phone: string | null;
17
17
  };
18
18
  headingsFound: Array<{
19
19
  level: string;
@@ -44,26 +44,24 @@ export async function validateAts(htmlPath: string): Promise<AtsValidationResult
44
44
  await page.goto(`file://${absoluteHtml}`, { waitUntil: 'networkidle' });
45
45
 
46
46
  const rawData = await page.evaluate(() => {
47
- const nameEl = document.querySelector('.candidate-name, h1');
48
- const name = nameEl ? (nameEl.textContent || '').trim() : null;
49
-
50
- const fullText = (document.body.innerText || '').trim();
47
+ const nameEl = document.querySelector('.candidate-name') || document.querySelector('h1');
48
+ const candidateName = nameEl ? nameEl.textContent?.trim() || null : null;
51
49
 
52
- // 提取电话与邮箱
53
- const phoneMatch = fullText.match(/(?:(?:\+|00)86)?1[3-9]\d{9}|(?:1[3-9]\d-\d{4}-\d{4})/);
54
- const emailMatch = fullText.match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/);
50
+ const bodyText = document.body.innerText || '';
51
+ const emailMatch = bodyText.match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/);
52
+ const phoneMatch = bodyText.match(/1[3-9]\d{9}|\+?\d{1,4}[-\s]?\d{7,11}/);
55
53
 
56
- const headings = Array.from(document.querySelectorAll('h1, h2, h3, .section-title, .sub-title')).map(h => ({
54
+ const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4')).map(h => ({
57
55
  level: h.tagName.toLowerCase(),
58
- text: (h.textContent || '').trim()
56
+ text: h.textContent?.replace(/[\s\n]+/g, ' ').trim() || ''
59
57
  }));
60
58
 
61
59
  return {
62
- candidateName: name,
63
- phone: phoneMatch ? phoneMatch[0] : null,
60
+ candidateName,
64
61
  email: emailMatch ? emailMatch[0] : null,
62
+ phone: phoneMatch ? phoneMatch[0] : null,
65
63
  headings,
66
- fullTextLength: fullText.length
64
+ totalTextLength: bodyText.trim().length
67
65
  };
68
66
  });
69
67
 
@@ -85,12 +83,11 @@ export async function validateAts(htmlPath: string): Promise<AtsValidationResult
85
83
  }
86
84
 
87
85
  const validatedHeadings = rawData.headings.map(h => {
88
- const textClean = h.text.toLowerCase().replace(/[^\w\u4e00-\u9fa5]/g, '');
89
- const isStd = STANDARD_SECTION_HEADERS.some(s => textClean.includes(s.toLowerCase().replace(/\s+/g, '')));
86
+ const textClean = h.text.toLowerCase();
87
+ const isStandard = STANDARD_SECTION_HEADERS.some(sh => textClean.includes(sh.toLowerCase()));
90
88
  return {
91
- level: h.level,
92
- text: h.text,
93
- isStandard: isStd
89
+ ...h,
90
+ isStandard
94
91
  };
95
92
  });
96
93
 
@@ -105,11 +102,11 @@ export async function validateAts(htmlPath: string): Promise<AtsValidationResult
105
102
  status,
106
103
  candidateName: rawData.candidateName,
107
104
  detectedContacts: {
108
- phone: rawData.phone,
109
- email: rawData.email
105
+ email: rawData.email,
106
+ phone: rawData.phone
110
107
  },
111
108
  headingsFound: validatedHeadings,
112
- totalTextLength: rawData.fullTextLength,
109
+ totalTextLength: rawData.totalTextLength,
113
110
  warnings,
114
111
  errors
115
112
  };
@@ -126,6 +123,8 @@ async function main() {
126
123
  i++;
127
124
  } else if (args[i] === '--json') {
128
125
  jsonOutput = true;
126
+ } else if (!args[i].startsWith('-')) {
127
+ htmlPath = args[i];
129
128
  }
130
129
  }
131
130
 
@@ -134,36 +133,41 @@ async function main() {
134
133
 
135
134
  if (jsonOutput) {
136
135
  console.log(JSON.stringify(result, null, 2));
137
- return;
136
+ process.exit(result.status === 'FAIL' ? 1 : 0);
138
137
  }
139
138
 
140
139
  console.log('======================================================');
141
140
  console.log(` KnowMe CareerForge — ATS Compliance QA: [${result.status}]`);
142
- console.log(` Candidate Name : ${result.candidateName || 'N/A'}`);
143
- console.log(` Phone Extracted: ${result.detectedContacts.phone || 'None'}`);
144
- console.log(` Email Extracted: ${result.detectedContacts.email || 'None'}`);
141
+ console.log(` Candidate Name : ${result.candidateName || 'NOT FOUND'}`);
142
+ console.log(` Phone Extracted: ${result.detectedContacts.phone || 'NOT DETECTED'}`);
143
+ console.log(` Email Extracted: ${result.detectedContacts.email || 'NOT DETECTED'}`);
145
144
  console.log(` Text Extracted : ${result.totalTextLength} characters`);
146
145
  console.log('------------------------------------------------------');
147
146
  console.log(' Headings Structure:');
148
147
  for (const h of result.headingsFound) {
149
- const tag = h.isStandard ? '✓' : '?';
150
- console.log(` [${tag}] <${h.level}> ${h.text}`);
148
+ const mark = h.isStandard ? '[✓]' : '[?]';
149
+ console.log(` ${mark} <${h.level}> ${h.text}`);
151
150
  }
152
151
 
153
- if (result.errors.length > 0) {
154
- console.log('\n[!] Errors:');
155
- for (const e of result.errors) console.log(` - ✗ ${e}`);
152
+ if (result.warnings.length > 0) {
153
+ console.log('\n[!] Warnings:');
154
+ for (const w of result.warnings) {
155
+ console.log(` - ⚠️ ${w}`);
156
+ }
156
157
  }
157
158
 
158
- if (result.warnings.length > 0) {
159
- console.log('\n[?] Warnings:');
160
- for (const w of result.warnings) console.log(` - ⚠️ ${w}`);
159
+ if (result.errors.length > 0) {
160
+ console.log('\n[✗] Errors:');
161
+ for (const e of result.errors) {
162
+ console.log(` - ✗ ${e}`);
163
+ }
161
164
  }
162
165
 
163
166
  console.log('======================================================');
167
+ process.exit(result.status === 'FAIL' ? 1 : 0);
164
168
  } catch (err: unknown) {
165
- const msg = err instanceof Error ? err.message : String(err);
166
- console.error(`[✗] ATS Validation Failed: ${msg}`);
169
+ const errorMsg = err instanceof Error ? err.message : String(err);
170
+ console.error(`[✗] ATS Validation Failed: ${errorMsg}`);
167
171
  process.exit(1);
168
172
  }
169
173
  }
@@ -14,15 +14,15 @@ interface LayoutValidationResult {
14
14
  totalPagesFound: number;
15
15
  pageDetails: Array<{
16
16
  pageIndex: number;
17
- measuredHeightPx: number;
17
+ actualHeightPx: number;
18
18
  allowedHeightPx: number;
19
- overflowPx: number;
20
19
  isOverflow: boolean;
20
+ overflowDeltaPx: number;
21
21
  }>;
22
22
  overflowSections: Array<{
23
23
  selector: string;
24
- heightPx: number;
25
- textPreview: string;
24
+ bottomPx: number;
25
+ overflowByPx: number;
26
26
  }>;
27
27
  }
28
28
 
@@ -35,7 +35,7 @@ export async function validateLayout(htmlPath: string, expectedPages: number = 1
35
35
  const browser = await chromium.launch({ headless: true });
36
36
  const context = await browser.newContext({
37
37
  viewport: { width: 794, height: 1123 }, // A4 at 96 DPI
38
- deviceScaleFactor: 2
38
+ deviceScaleFactor: 1
39
39
  });
40
40
 
41
41
  const page = await context.newPage();
@@ -47,39 +47,44 @@ export async function validateLayout(htmlPath: string, expectedPages: number = 1
47
47
 
48
48
  const evaluation = await page.evaluate((allowedHeight) => {
49
49
  const pages = Array.from(document.querySelectorAll('.resume-page'));
50
- const details = pages.map((el, index) => {
51
- const rect = el.getBoundingClientRect();
52
- const scrollH = el.scrollHeight;
53
- const actualH = Math.max(rect.height, scrollH);
54
- const diff = actualH - allowedHeight;
50
+ const details = pages.map((p, idx) => {
51
+ const rect = p.getBoundingClientRect();
52
+ const actualHeight = Math.ceil(rect.height);
53
+ const isOverflow = actualHeight > allowedHeight + 2; // 2px 浮点容差
55
54
  return {
56
- pageIndex: index + 1,
57
- measuredHeightPx: Math.round(actualH * 10) / 10,
55
+ pageIndex: idx + 1,
56
+ actualHeightPx: actualHeight,
58
57
  allowedHeightPx: allowedHeight,
59
- overflowPx: diff > 2 ? Math.round(diff * 10) / 10 : 0,
60
- isOverflow: diff > 2
58
+ isOverflow,
59
+ overflowDeltaPx: isOverflow ? Math.ceil(actualHeight - allowedHeight) : 0
61
60
  };
62
61
  });
63
62
 
64
- const overflows: Array<{ selector: string; heightPx: number; textPreview: string }> = [];
63
+ const overflowSections: Array<{ selector: string; bottomPx: number; overflowByPx: number }> = [];
65
64
  if (details.some(d => d.isOverflow)) {
66
- const sections = Array.from(document.querySelectorAll('.resume-section, .timeline-item, .milestone-item, .experience-item, tr'));
67
- for (const s of sections) {
68
- const r = s.getBoundingClientRect();
69
- if (r.bottom > allowedHeight) {
70
- overflows.push({
71
- selector: s.className || s.tagName.toLowerCase(),
72
- heightPx: Math.round(r.height),
73
- textPreview: (s.textContent || '').trim().slice(0, 60)
65
+ const items = Array.from(document.querySelectorAll('.experience-item, .project-item, .skills-content, .resume-section'));
66
+ for (const item of items) {
67
+ const rect = item.getBoundingClientRect();
68
+ if (rect.bottom > allowedHeight) {
69
+ overflowSections.push({
70
+ selector: item.className ? `.${item.className.split(' ').join('.')}` : item.tagName.toLowerCase(),
71
+ bottomPx: Math.ceil(rect.bottom),
72
+ overflowByPx: Math.ceil(rect.bottom - allowedHeight)
74
73
  });
75
74
  }
76
75
  }
77
76
  }
78
77
 
79
78
  return {
80
- totalPagesFound: pages.length,
81
- pageDetails: details,
82
- overflowSections: overflows
79
+ totalPagesFound: pages.length || 1,
80
+ pageDetails: details.length ? details : [{
81
+ pageIndex: 1,
82
+ actualHeightPx: Math.ceil(document.body.getBoundingClientRect().height),
83
+ allowedHeightPx: allowedHeight,
84
+ isOverflow: document.body.getBoundingClientRect().height > allowedHeight + 2,
85
+ overflowDeltaPx: Math.max(0, Math.ceil(document.body.getBoundingClientRect().height - allowedHeight))
86
+ }],
87
+ overflowSections
83
88
  };
84
89
  }, A4_HEIGHT_PX);
85
90
 
@@ -111,6 +116,8 @@ async function main() {
111
116
  i++;
112
117
  } else if (args[i] === '--json') {
113
118
  jsonOutput = true;
119
+ } else if (!args[i].startsWith('-')) {
120
+ htmlPath = args[i];
114
121
  }
115
122
  }
116
123
 
@@ -119,7 +126,7 @@ async function main() {
119
126
 
120
127
  if (jsonOutput) {
121
128
  console.log(JSON.stringify(result, null, 2));
122
- return;
129
+ process.exit(result.status === 'PASS' ? 0 : 1);
123
130
  }
124
131
 
125
132
  console.log('======================================================');
@@ -129,29 +136,23 @@ async function main() {
129
136
  console.log('------------------------------------------------------');
130
137
 
131
138
  for (const p of result.pageDetails) {
132
- const statusSymbol = p.isOverflow ? '✗ OVERFLOW' : '✓ FIT';
133
- console.log(` Page ${p.pageIndex}: ${p.measuredHeightPx}px / ${p.allowedHeightPx}px [${statusSymbol}] (Overflow: ${p.overflowPx}px)`);
139
+ const fitText = p.isOverflow ? `[✗ OVERFLOW by ${p.overflowDeltaPx}px]` : `[✓ FIT] (Overflow: ${p.overflowDeltaPx}px)`;
140
+ console.log(` Page ${p.pageIndex}: ${p.actualHeightPx}px / ${p.allowedHeightPx}px ${fitText}`);
134
141
  }
135
142
 
136
143
  if (result.overflowSections.length > 0) {
137
- console.log('\n[!] Nodes exceeding A4 physical boundary:');
138
- for (const o of result.overflowSections) {
139
- console.log(` - <${o.selector}> (${o.heightPx}px): "${o.textPreview}..."`);
144
+ console.log('\n[!] Offending Overflow Elements:');
145
+ for (const el of result.overflowSections.slice(0, 3)) {
146
+ console.log(` - Element "${el.selector}" exceeds A4 baseline by ${el.overflowByPx}px (Bottom: ${el.bottomPx}px)`);
140
147
  }
141
- console.log('\n[Self-Healing Guidance]:');
142
- console.log(' 1. Reduce --resume-space-section by 1~2pt in workspace/resume.html <style>');
143
- console.log(' 2. Reduce --resume-font-size-body by 0.2pt');
144
- console.log(' 3. Condense verbose bullet points');
148
+ console.log('\n💡 Recommendation: Reduce --resume-space-section by 1~2pt or reduce --resume-font-size-body by 0.2pt in <style>.');
145
149
  }
146
150
 
147
151
  console.log('======================================================');
148
-
149
- if (result.status === 'OVERFLOW') {
150
- process.exit(1);
151
- }
152
+ process.exit(result.status === 'PASS' ? 0 : 1);
152
153
  } catch (err: unknown) {
153
- const msg = err instanceof Error ? err.message : String(err);
154
- console.error(`[✗] Layout Validation Failed: ${msg}`);
154
+ const errorMsg = err instanceof Error ? err.message : String(err);
155
+ console.error(`[✗] Layout Validation Failed: ${errorMsg}`);
155
156
  process.exit(1);
156
157
  }
157
158
  }
@@ -7,21 +7,28 @@ KnowMe CareerForge — Resume Validation Engine (Layout, Schema & ATS Compliance
7
7
  import sys, os, json, argparse, re
8
8
  from pathlib import Path
9
9
 
10
+ def get_project_root() -> Path:
11
+ curr = Path(__file__).resolve().parent
12
+ for _ in range(5):
13
+ if (curr / "SKILL.md").exists() or (curr / "package.json").exists():
14
+ return curr
15
+ curr = curr.parent
16
+ return Path.cwd()
17
+
10
18
  def validate_resume_html(html_path: str, expected_pages: int = 1) -> dict:
11
19
  p = Path(html_path)
12
20
  if not p.exists():
13
- # 尝试相对于脚本目录上一级
14
- alt = Path(__file__).resolve().parent.parent / html_path
21
+ root = get_project_root()
22
+ alt = root / html_path
15
23
  if alt.exists():
16
24
  p = alt
17
25
  else:
18
26
  return {
19
27
  "status": "FAIL",
20
- "file": html_path,
21
- "expectedPages": expected_pages,
22
- "checks": {},
23
- "errors": [f"HTML file not found at: {html_path}"],
24
- "warnings": []
28
+ "file": str(p),
29
+ "errors": [f"File not found: {html_path}"],
30
+ "warnings": [],
31
+ "checks": {}
25
32
  }
26
33
 
27
34
  content = p.read_text(encoding="utf-8")
@@ -45,7 +52,9 @@ def validate_resume_html(html_path: str, expected_pages: int = 1) -> dict:
45
52
  required_tokens = [
46
53
  "--resume-page-width",
47
54
  "--resume-page-min-height",
48
- "--resume-font-size-body"
55
+ "--resume-font-size-body",
56
+ "--resume-space-section",
57
+ "--resume-color-primary"
49
58
  ]
50
59
  missing_tokens = []
51
60
  for token in required_tokens:
@@ -72,18 +81,18 @@ def validate_resume_html(html_path: str, expected_pages: int = 1) -> dict:
72
81
 
73
82
  checks["detected_sections"] = detected_sections
74
83
 
75
- # 6. 单页高度预估 (剔除 <style> 后的实际可见文字量)
84
+ # 6. 单页高度预估 (可见文字量)
76
85
  body_text_only = re.sub(r'<style.*?>.*?</style>', '', content, flags=re.DOTALL)
77
86
  visible_text = re.sub(r'<[^>]+>', '', body_text_only).strip()
78
87
  char_count = len(re.sub(r'\s+', ' ', visible_text))
79
88
 
80
89
  density_status = "OPTIMAL"
81
90
  if expected_pages == 1:
82
- if char_count > 1500:
83
- warnings.append(f"Visible text volume ({char_count} chars) is high for a single-page resume; risk of overflow.")
84
- density_status = "HIGH_RISK_OVERFLOW"
85
- elif char_count < 400:
86
- warnings.append(f"Visible text volume ({char_count} chars) is low for a single-page resume; canvas may appear sparse.")
91
+ if char_count > 1600:
92
+ warnings.append(f"High text density for 1 page: {char_count} chars (recommend trimming or reducing spacing tokens)")
93
+ density_status = "OVERFLOW_RISK"
94
+ elif char_count < 300:
95
+ warnings.append(f"Low content volume for 1 page: {char_count} chars")
87
96
  density_status = "UNDERFLOW"
88
97
 
89
98
  checks["char_count"] = char_count
@@ -95,21 +104,21 @@ def validate_resume_html(html_path: str, expected_pages: int = 1) -> dict:
95
104
  "status": "PASS" if passed else "FAIL",
96
105
  "file": str(p.resolve()),
97
106
  "expectedPages": expected_pages,
98
- "checks": checks,
99
107
  "errors": errors,
100
- "warnings": warnings
108
+ "warnings": warnings,
109
+ "checks": checks
101
110
  }
102
111
 
103
112
  def main():
104
113
  parser = argparse.ArgumentParser(description="KnowMe CareerForge — Resume Validator")
105
- parser.add_argument("--html", "-i", default="workspace/resume.html", help="Path to resume HTML (default: workspace/resume.html)")
106
- parser.add_argument("--expected-pages", "-p", type=int, default=1, help="Expected page count (default: 1)")
107
- parser.add_argument("--json", action="store_true", help="Output JSON format")
114
+ parser.add_argument("path", nargs="?", default="workspace/resume.html", help="Path to resume.html")
115
+ parser.add_argument("--html", help="Path to resume.html (named)")
116
+ parser.add_argument("--expected-pages", "-p", type=int, default=1, help="Expected target pages (1 or 2)")
117
+ parser.add_argument("--json", action="store_true", help="Output JSON result")
108
118
 
109
119
  args = parser.parse_args()
110
-
111
- result = validate_resume_html(args.html, args.expected_pages)
112
-
120
+ target_html = args.html or args.path
121
+ result = validate_resume_html(target_html, expected_pages=args.expected_pages)
113
122
  if args.json:
114
123
  print(json.dumps(result, ensure_ascii=False, indent=2))
115
124
  return
package/skill.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/skill.json",
3
3
  "name": "knowme-careerforge",
4
4
  "displayName": "KnowMe CareerForge",
5
- "version": "0.0.1",
5
+ "version": "0.0.2",
6
6
  "description": "An agent-native skill for self-discovery, career positioning, and tailored resume engineering. Know yourself. Define your direction. Forge your opportunity.",
7
7
  "author": "Miles <https://github.com/Max-Samson>",
8
8
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.1",
2
+ "version": "0.0.2",
3
3
  "totalRoles": 9,
4
4
  "totalTemplates": 4,
5
5
  "roles": [
@@ -0,0 +1,112 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "CandidateResumeSchema",
4
+ "description": "KnowMe CareerForge Candidate Master Profile & Tailored Resume Schema with L1~L3 Evidence Chains.",
5
+ "type": "object",
6
+ "required": ["basics", "skills", "experience", "education"],
7
+ "properties": {
8
+ "basics": {
9
+ "type": "object",
10
+ "required": ["name", "title", "email", "phone"],
11
+ "properties": {
12
+ "name": { "type": "string", "description": "Candidate's full name" },
13
+ "title": { "type": "string", "description": "Professional job title or target role" },
14
+ "email": { "type": "string", "format": "email" },
15
+ "phone": { "type": "string" },
16
+ "location": { "type": "string" },
17
+ "website": { "type": "string" },
18
+ "github": { "type": "string" },
19
+ "summary": { "type": "string", "description": "Core Value Proposition (15-30 words)" }
20
+ }
21
+ },
22
+ "skills": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["category", "items"],
27
+ "properties": {
28
+ "category": { "type": "string" },
29
+ "items": { "type": "array", "items": { "type": "string" } },
30
+ "highlighted": { "type": "array", "items": { "type": "string" } }
31
+ }
32
+ }
33
+ },
34
+ "experience": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["company", "role", "startDate", "endDate", "bullets"],
39
+ "properties": {
40
+ "company": { "type": "string" },
41
+ "role": { "type": "string" },
42
+ "startDate": { "type": "string" },
43
+ "endDate": { "type": "string" },
44
+ "location": { "type": "string" },
45
+ "summary": { "type": "string" },
46
+ "bullets": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "object",
50
+ "required": ["text", "evidenceLevel"],
51
+ "properties": {
52
+ "text": { "type": "string" },
53
+ "evidenceLevel": { "type": "string", "enum": ["L1", "L2", "L3"] },
54
+ "evidenceSource": { "type": "string" },
55
+ "keywords": { "type": "array", "items": { "type": "string" } }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "projects": {
63
+ "type": "array",
64
+ "items": {
65
+ "type": "object",
66
+ "required": ["name", "role", "techStack", "bullets"],
67
+ "properties": {
68
+ "name": { "type": "string" },
69
+ "role": { "type": "string" },
70
+ "techStack": { "type": "array", "items": { "type": "string" } },
71
+ "repoUrl": { "type": "string" },
72
+ "demoUrl": { "type": "string" },
73
+ "startDate": { "type": "string" },
74
+ "endDate": { "type": "string" },
75
+ "bullets": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": "object",
79
+ "required": ["text", "evidenceLevel"],
80
+ "properties": {
81
+ "text": { "type": "string" },
82
+ "evidenceLevel": { "type": "string", "enum": ["L1", "L2", "L3"] },
83
+ "evidenceSource": { "type": "string" },
84
+ "keywords": { "type": "array", "items": { "type": "string" } }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ },
91
+ "education": {
92
+ "type": "array",
93
+ "items": {
94
+ "type": "object",
95
+ "required": ["institution", "degree", "startDate", "endDate"],
96
+ "properties": {
97
+ "institution": { "type": "string" },
98
+ "degree": { "type": "string" },
99
+ "field": { "type": "string" },
100
+ "startDate": { "type": "string" },
101
+ "endDate": { "type": "string" },
102
+ "gpa": { "type": "string" },
103
+ "honors": { "type": "array", "items": { "type": "string" } }
104
+ }
105
+ }
106
+ },
107
+ "certifications": {
108
+ "type": "array",
109
+ "items": { "type": "string" }
110
+ }
111
+ }
112
+ }
@@ -8,11 +8,11 @@
8
8
 
9
9
  1. **执行模板检索**:
10
10
  ```bash
11
- python3 scripts/search-template.py "<Target Role>" --style "<minimal|modern|executive|classic>" --target-pages 1
11
+ python3 scripts/template/search-template.py "<Target Role>" --style "<minimal|modern|executive|classic>" --target-pages 1
12
12
  ```
13
13
  2. **实例化中间修改场**:
14
14
  ```bash
15
- python3 scripts/instantiate-resume.py --template <template_id> --keywords "Python,LLM,RAG,FastAPI" --output workspace/resume.html
15
+ python3 scripts/template/instantiate-resume.py --template <template_id> --keywords "Python,LLM,RAG,FastAPI" --output workspace/resume.html
16
16
  ```
17
17
 
18
18
  ## 2. 修改场编辑规范
@@ -8,12 +8,12 @@
8
8
 
9
9
  1. **测试 1:布局与 DOM 高度溢出检测**:
10
10
  ```bash
11
- python3 scripts/validate-resume.py --html workspace/resume.html --expected-pages 1
11
+ python3 scripts/validation/validate-resume.py --html workspace/resume.html --expected-pages 1
12
12
  ```
13
13
  - 单页标准高度:$297\text{mm} \times 96\text{DPI} / 25.4 \approx 1122.5\text{px}$。
14
14
  2. **测试 2:ATS 纯文本流解析检测**:
15
15
  ```bash
16
- npx ts-node scripts/validate-ats.ts --html workspace/resume.html
16
+ npx ts-node scripts/validation/validate-ats.ts --html workspace/resume.html
17
17
  ```
18
18
 
19
19
  ## 2. 自动化自愈修复闭环
@@ -27,5 +27,5 @@
27
27
  ## 3. 确定性无损 PDF 导出
28
28
 
29
29
  ```bash
30
- npx ts-node scripts/render-pdf.ts --input workspace/resume.html --output workspace/resume.pdf
30
+ python3 scripts/rendering/render-pdf.py workspace/resume.html workspace/resume.pdf
31
31
  ```
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- KnowMe CareerForge — Resume Workspace Instantiator
4
- 将指定模板与关键词高亮组装为自包含的单文件 HTML 工作区修改场 (Intermediate Working Canvas)。
5
- """
6
-
7
- import sys, os, json, argparse, re
8
- from pathlib import Path
9
-
10
- def highlight_keywords(text, keywords):
11
- if not keywords:
12
- return text
13
- for kw in keywords:
14
- kw_clean = kw.strip()
15
- if kw_clean:
16
- # 避免对已有标签属性进行错误替换
17
- pattern = re.compile(rf'(?<![<\w/])({re.escape(kw_clean)})(?![>\w])', re.IGNORECASE)
18
- text = pattern.sub(lambda m: f"<strong>{m.group(0)}</strong>", text)
19
- return text
20
-
21
- def instantiate_workspace(template_id, keywords=None, output_path="workspace/resume.html"):
22
- script_dir = Path(__file__).resolve().parent
23
- base_dir = script_dir.parent
24
-
25
- template_dir = base_dir / "src" / "templates" / template_id
26
-
27
- if not template_dir.exists():
28
- # 尝试通过 ID 查找
29
- for sub in (base_dir / "src" / "templates").iterdir():
30
- if sub.is_dir() and (sub / "metadata.json").exists():
31
- try:
32
- meta = json.loads((sub / "metadata.json").read_text(encoding="utf-8"))
33
- if meta.get("id") == template_id:
34
- template_dir = sub
35
- break
36
- except:
37
- pass
38
-
39
- if not template_dir.exists():
40
- raise ValueError(f"Template '{template_id}' not found in src/templates/")
41
-
42
- html_file = template_dir / "template.html"
43
- css_file = template_dir / "style.css"
44
-
45
- if not html_file.exists() or not css_file.exists():
46
- raise FileNotFoundError(f"template.html or style.css missing in {template_dir}")
47
-
48
- html_content = html_file.read_text(encoding="utf-8")
49
- css_content = css_file.read_text(encoding="utf-8")
50
-
51
- # 将外部 style.css 内联进 HTML,形成零依赖单文件工作场
52
- inlined_html = html_content.replace(
53
- '<link rel="stylesheet" href="style.css">',
54
- f'<style>\n{css_content}\n </style>'
55
- )
56
-
57
- # 注入关键词高亮
58
- if keywords:
59
- kw_list = [k.strip() for k in keywords.split(",") if k.strip()]
60
- inlined_html = highlight_keywords(inlined_html, kw_list)
61
-
62
- out_file = Path(output_path)
63
- out_file.parent.mkdir(parents=True, exist_ok=True)
64
- out_file.write_text(inlined_html, encoding="utf-8")
65
-
66
- print("=" * 70)
67
- print(f" KnowMe CareerForge — Intermediate Canvas Instantiated!")
68
- print(f" Selected Template : {template_id} ({template_dir.name})")
69
- print(f" Working Canvas : {out_file.resolve()}")
70
- print("=" * 70)
71
- return out_file
72
-
73
- def main():
74
- parser = argparse.ArgumentParser(description="KnowMe CareerForge — Workspace Instantiator")
75
- parser.add_argument("--template", "-t", required=True, help="Template ID (minimal, modern, executive, classic)")
76
- parser.add_argument("--keywords", "-k", default=None, help="Comma-separated keywords to highlight (e.g. 'Python,LLM,RAG')")
77
- parser.add_argument("--output", "-o", default="workspace/resume.html", help="Output canvas path (default: workspace/resume.html)")
78
-
79
- args = parser.parse_args()
80
- instantiate_workspace(args.template, keywords=args.keywords, output_path=args.output)
81
-
82
- if __name__ == "__main__":
83
- main()