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.
- package/README.md +6 -9
- package/README.zh-CN.md +6 -9
- package/SKILL.md +95 -116
- package/agents/claude/knowme-careerforge.md +9 -8
- package/agents/codex/knowme-careerforge.yaml +15 -6
- package/agents/cursor/knowme-careerforge.mdc +7 -6
- package/agents/opencode/skill.yaml +1 -1
- package/agents/windsurf/knowme-careerforge.rules +11 -8
- package/dist/cli/src/commands/init.d.ts +1 -1
- package/dist/cli/src/commands/init.js +37 -10
- package/dist/cli/src/index.js +65 -22
- package/dist/scripts/render-pdf.d.ts +0 -9
- package/dist/scripts/render-pdf.js +5 -68
- package/dist/scripts/rendering/render-pdf.d.ts +11 -0
- package/dist/scripts/rendering/render-pdf.js +109 -0
- package/dist/scripts/validate-ats.d.ts +0 -21
- package/dist/scripts/validate-ats.js +5 -132
- package/dist/scripts/validate-layout.d.ts +0 -22
- package/dist/scripts/validate-layout.js +5 -124
- package/dist/scripts/validation/validate-ats.d.ts +23 -0
- package/dist/scripts/validation/validate-ats.js +173 -0
- package/dist/scripts/validation/validate-layout.d.ts +24 -0
- package/dist/scripts/validation/validate-layout.js +164 -0
- package/package.json +15 -12
- package/scripts/Agent.md +138 -0
- package/scripts/{build-gallery.py → build/build-gallery.py} +128 -99
- package/scripts/{build-knowledge.py → build/build-knowledge.py} +9 -1
- package/scripts/{release.py → build/release.py} +20 -16
- package/scripts/{run-all-tests.py → build/run-all-tests.py} +9 -1
- package/scripts/build/sync-version.py +107 -0
- package/scripts/{analyze-jd.py → evidence/analyze-jd.py} +34 -45
- package/scripts/evidence/extract-evidence.py +348 -0
- package/scripts/pipeline/forge.py +142 -0
- package/scripts/rendering/render-pdf.py +162 -0
- package/scripts/{render-pdf.ts → rendering/render-pdf.ts} +3 -3
- package/scripts/template/instantiate-resume.py +178 -0
- package/scripts/{search-template.py → template/search-template.py} +44 -42
- package/scripts/{validate-ats.ts → validation/validate-ats.ts} +39 -35
- package/scripts/{validate-layout.ts → validation/validate-layout.ts} +43 -42
- package/scripts/{validate-resume.py → validation/validate-resume.py} +31 -22
- package/skill.json +1 -1
- package/src/knowledge/index.json +1 -1
- package/src/knowledge/resume-schema.json +112 -0
- package/src/workflows/forge-resume.md +2 -2
- package/src/workflows/review.md +3 -3
- package/scripts/instantiate-resume.py +0 -83
- 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
|
|
48
|
-
const
|
|
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
|
|
54
|
-
const
|
|
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,
|
|
54
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4')).map(h => ({
|
|
57
55
|
level: h.tagName.toLowerCase(),
|
|
58
|
-
text:
|
|
56
|
+
text: h.textContent?.replace(/[\s\n]+/g, ' ').trim() || ''
|
|
59
57
|
}));
|
|
60
58
|
|
|
61
59
|
return {
|
|
62
|
-
candidateName
|
|
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
|
-
|
|
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()
|
|
89
|
-
const
|
|
86
|
+
const textClean = h.text.toLowerCase();
|
|
87
|
+
const isStandard = STANDARD_SECTION_HEADERS.some(sh => textClean.includes(sh.toLowerCase()));
|
|
90
88
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
109
|
-
|
|
105
|
+
email: rawData.email,
|
|
106
|
+
phone: rawData.phone
|
|
110
107
|
},
|
|
111
108
|
headingsFound: validatedHeadings,
|
|
112
|
-
totalTextLength: rawData.
|
|
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
|
-
|
|
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 || '
|
|
143
|
-
console.log(` Phone Extracted: ${result.detectedContacts.phone || '
|
|
144
|
-
console.log(` Email Extracted: ${result.detectedContacts.email || '
|
|
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
|
|
150
|
-
console.log(`
|
|
148
|
+
const mark = h.isStandard ? '[✓]' : '[?]';
|
|
149
|
+
console.log(` ${mark} <${h.level}> ${h.text}`);
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
if (result.
|
|
154
|
-
console.log('\n[!]
|
|
155
|
-
for (const
|
|
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.
|
|
159
|
-
console.log('\n[
|
|
160
|
-
for (const
|
|
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
|
|
166
|
-
console.error(`[✗] ATS Validation Failed: ${
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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:
|
|
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((
|
|
51
|
-
const rect =
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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:
|
|
57
|
-
|
|
55
|
+
pageIndex: idx + 1,
|
|
56
|
+
actualHeightPx: actualHeight,
|
|
58
57
|
allowedHeightPx: allowedHeight,
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
isOverflow,
|
|
59
|
+
overflowDeltaPx: isOverflow ? Math.ceil(actualHeight - allowedHeight) : 0
|
|
61
60
|
};
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
const
|
|
63
|
+
const overflowSections: Array<{ selector: string; bottomPx: number; overflowByPx: number }> = [];
|
|
65
64
|
if (details.some(d => d.isOverflow)) {
|
|
66
|
-
const
|
|
67
|
-
for (const
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
selector:
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
133
|
-
console.log(` Page ${p.pageIndex}: ${p.
|
|
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[!]
|
|
138
|
-
for (const
|
|
139
|
-
console.log(` -
|
|
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
|
|
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
|
|
154
|
-
console.error(`[✗] Layout Validation Failed: ${
|
|
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 =
|
|
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":
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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. 单页高度预估 (
|
|
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 >
|
|
83
|
-
warnings.append(f"
|
|
84
|
-
density_status = "
|
|
85
|
-
elif char_count <
|
|
86
|
-
warnings.append(f"
|
|
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("
|
|
106
|
-
parser.add_argument("--
|
|
107
|
-
parser.add_argument("--
|
|
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(
|
|
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.
|
|
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",
|
package/src/knowledge/index.json
CHANGED
|
@@ -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. 修改场编辑规范
|
package/src/workflows/review.md
CHANGED
|
@@ -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
|
-
|
|
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()
|