knowme-careerforge-skill 0.0.1 → 0.0.4
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 +105 -116
- package/agents/claude/knowme-careerforge.md +11 -9
- package/agents/codex/knowme-careerforge.yaml +15 -6
- package/agents/cursor/knowme-careerforge.mdc +12 -7
- 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 +46 -10
- package/dist/cli/src/index.js +82 -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 +16 -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 +116 -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 +184 -0
- package/scripts/template/search-template.py +350 -0
- 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 +2 -10
- package/src/knowledge/resume-schema.json +112 -0
- package/src/knowledge/templates.json +1 -9
- package/src/references/01-evidence-mining.md +41 -0
- package/src/references/02-career-goal.md +38 -0
- package/src/references/03-jd-analysis.md +44 -0
- package/src/references/04-template-selection.md +43 -0
- package/src/references/05-html-canvas-tokens.md +51 -0
- package/src/references/06-qa-and-rendering.md +54 -0
- package/src/templates/classic/style.css +22 -51
- package/src/templates/common/base.css +150 -0
- package/src/templates/executive/style.css +30 -73
- package/src/templates/minimal/style.css +30 -72
- package/src/templates/modern/style.css +32 -72
- package/scripts/instantiate-resume.py +0 -83
- package/scripts/search-template.py +0 -147
- package/scripts/sync-version.py +0 -57
- package/src/references/ats.md +0 -17
- package/src/references/evidence-first.md +0 -22
- package/src/references/pdf.md +0 -40
- package/src/references/resume-writing.md +0 -25
- package/src/references/template-design.md +0 -57
- package/src/workflows/analyze-job.md +0 -16
- package/src/workflows/define-goal.md +0 -16
- package/src/workflows/forge-resume.md +0 -25
- package/src/workflows/know-me.md +0 -21
- package/src/workflows/position.md +0 -19
- package/src/workflows/review.md +0 -31
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ts-node
|
|
2
2
|
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* KnowMe CareerForge — Playwright DOM Layout & Overflow Inspector
|
|
5
|
-
* 基于无头浏览器真实渲染计算 DOM 盒模型高度,检测单页/双页物理 A4 溢出与孤行。
|
|
6
|
-
*/
|
|
7
3
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
4
|
if (k2 === undefined) k2 = k;
|
|
9
5
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -38,124 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
34
|
};
|
|
39
35
|
})();
|
|
40
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
|
|
42
|
-
const playwright_1 = require("playwright");
|
|
37
|
+
/** Forwarding shim for scripts/validation/validate-layout.ts */
|
|
43
38
|
const path = __importStar(require("path"));
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error(`HTML file not found: ${absoluteHtml}`);
|
|
49
|
-
}
|
|
50
|
-
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
51
|
-
const context = await browser.newContext({
|
|
52
|
-
viewport: { width: 794, height: 1123 }, // A4 at 96 DPI
|
|
53
|
-
deviceScaleFactor: 2
|
|
54
|
-
});
|
|
55
|
-
const page = await context.newPage();
|
|
56
|
-
await page.goto(`file://${absoluteHtml}`, { waitUntil: 'networkidle' });
|
|
57
|
-
await page.evaluateHandle('document.fonts.ready');
|
|
58
|
-
// A4 标准高度 (297mm in pixels at 96 DPI ~= 1122.5px)
|
|
59
|
-
const A4_HEIGHT_PX = 1122.5;
|
|
60
|
-
const evaluation = await page.evaluate((allowedHeight) => {
|
|
61
|
-
const pages = Array.from(document.querySelectorAll('.resume-page'));
|
|
62
|
-
const details = pages.map((el, index) => {
|
|
63
|
-
const rect = el.getBoundingClientRect();
|
|
64
|
-
const scrollH = el.scrollHeight;
|
|
65
|
-
const actualH = Math.max(rect.height, scrollH);
|
|
66
|
-
const diff = actualH - allowedHeight;
|
|
67
|
-
return {
|
|
68
|
-
pageIndex: index + 1,
|
|
69
|
-
measuredHeightPx: Math.round(actualH * 10) / 10,
|
|
70
|
-
allowedHeightPx: allowedHeight,
|
|
71
|
-
overflowPx: diff > 2 ? Math.round(diff * 10) / 10 : 0,
|
|
72
|
-
isOverflow: diff > 2
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
const overflows = [];
|
|
76
|
-
if (details.some(d => d.isOverflow)) {
|
|
77
|
-
const sections = Array.from(document.querySelectorAll('.resume-section, .timeline-item, .milestone-item, .experience-item, tr'));
|
|
78
|
-
for (const s of sections) {
|
|
79
|
-
const r = s.getBoundingClientRect();
|
|
80
|
-
if (r.bottom > allowedHeight) {
|
|
81
|
-
overflows.push({
|
|
82
|
-
selector: s.className || s.tagName.toLowerCase(),
|
|
83
|
-
heightPx: Math.round(r.height),
|
|
84
|
-
textPreview: (s.textContent || '').trim().slice(0, 60)
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
totalPagesFound: pages.length,
|
|
91
|
-
pageDetails: details,
|
|
92
|
-
overflowSections: overflows
|
|
93
|
-
};
|
|
94
|
-
}, A4_HEIGHT_PX);
|
|
95
|
-
await browser.close();
|
|
96
|
-
const hasOverflow = evaluation.pageDetails.some(p => p.isOverflow) || evaluation.totalPagesFound > expectedPages;
|
|
97
|
-
return {
|
|
98
|
-
status: hasOverflow ? 'OVERFLOW' : 'PASS',
|
|
99
|
-
expectedPages,
|
|
100
|
-
totalPagesFound: evaluation.totalPagesFound,
|
|
101
|
-
pageDetails: evaluation.pageDetails,
|
|
102
|
-
overflowSections: evaluation.overflowSections
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
async function main() {
|
|
106
|
-
const args = process.argv.slice(2);
|
|
107
|
-
let htmlPath = 'workspace/resume.html';
|
|
108
|
-
let expectedPages = 1;
|
|
109
|
-
let jsonOutput = false;
|
|
110
|
-
for (let i = 0; i < args.length; i++) {
|
|
111
|
-
if (args[i] === '--html' || args[i] === '-i') {
|
|
112
|
-
htmlPath = args[i + 1];
|
|
113
|
-
i++;
|
|
114
|
-
}
|
|
115
|
-
else if (args[i] === '--expected-pages' || args[i] === '-p') {
|
|
116
|
-
expectedPages = parseInt(args[i + 1], 10) || 1;
|
|
117
|
-
i++;
|
|
118
|
-
}
|
|
119
|
-
else if (args[i] === '--json') {
|
|
120
|
-
jsonOutput = true;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
try {
|
|
124
|
-
const result = await validateLayout(htmlPath, expectedPages);
|
|
125
|
-
if (jsonOutput) {
|
|
126
|
-
console.log(JSON.stringify(result, null, 2));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
console.log('======================================================');
|
|
130
|
-
console.log(` KnowMe CareerForge — Layout QA: ${result.status}`);
|
|
131
|
-
console.log(` HTML Path : ${htmlPath}`);
|
|
132
|
-
console.log(` Expected Pages : ${result.expectedPages} | Pages Found: ${result.totalPagesFound}`);
|
|
133
|
-
console.log('------------------------------------------------------');
|
|
134
|
-
for (const p of result.pageDetails) {
|
|
135
|
-
const statusSymbol = p.isOverflow ? '✗ OVERFLOW' : '✓ FIT';
|
|
136
|
-
console.log(` Page ${p.pageIndex}: ${p.measuredHeightPx}px / ${p.allowedHeightPx}px [${statusSymbol}] (Overflow: ${p.overflowPx}px)`);
|
|
137
|
-
}
|
|
138
|
-
if (result.overflowSections.length > 0) {
|
|
139
|
-
console.log('\n[!] Nodes exceeding A4 physical boundary:');
|
|
140
|
-
for (const o of result.overflowSections) {
|
|
141
|
-
console.log(` - <${o.selector}> (${o.heightPx}px): "${o.textPreview}..."`);
|
|
142
|
-
}
|
|
143
|
-
console.log('\n[Self-Healing Guidance]:');
|
|
144
|
-
console.log(' 1. Reduce --resume-space-section by 1~2pt in workspace/resume.html <style>');
|
|
145
|
-
console.log(' 2. Reduce --resume-font-size-body by 0.2pt');
|
|
146
|
-
console.log(' 3. Condense verbose bullet points');
|
|
147
|
-
}
|
|
148
|
-
console.log('======================================================');
|
|
149
|
-
if (result.status === 'OVERFLOW') {
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
-
console.error(`[✗] Layout Validation Failed: ${msg}`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (require.main === module) {
|
|
160
|
-
main();
|
|
161
|
-
}
|
|
39
|
+
const child_process = __importStar(require("child_process"));
|
|
40
|
+
const target = path.join(__dirname, 'validation', 'validate-layout.ts');
|
|
41
|
+
const proc = child_process.spawnSync('ts-node', [target, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
42
|
+
process.exit(proc.status || 0);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
/**
|
|
3
|
+
* KnowMe CareerForge — ATS Text Flow & Heading Hierarchy Inspector
|
|
4
|
+
* 验证 HTML 简历中的关键文本可提取性、标头层级规范与 ATS 机器解析友好度。
|
|
5
|
+
*/
|
|
6
|
+
interface AtsValidationResult {
|
|
7
|
+
status: 'PASS' | 'WARN' | 'FAIL';
|
|
8
|
+
candidateName: string | null;
|
|
9
|
+
detectedContacts: {
|
|
10
|
+
email: string | null;
|
|
11
|
+
phone: string | null;
|
|
12
|
+
};
|
|
13
|
+
headingsFound: Array<{
|
|
14
|
+
level: string;
|
|
15
|
+
text: string;
|
|
16
|
+
isStandard: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
totalTextLength: number;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
errors: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function validateAts(htmlPath: string): Promise<AtsValidationResult>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* KnowMe CareerForge — ATS Text Flow & Heading Hierarchy Inspector
|
|
5
|
+
* 验证 HTML 简历中的关键文本可提取性、标头层级规范与 ATS 机器解析友好度。
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.validateAts = validateAts;
|
|
42
|
+
const playwright_1 = require("playwright");
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const STANDARD_SECTION_HEADERS = [
|
|
46
|
+
'工作经历', '工作经验', 'work experience', 'experience',
|
|
47
|
+
'项目经历', '核心项目', 'projects', 'key projects',
|
|
48
|
+
'专业技能', '核心技能', 'skills', 'technical skills',
|
|
49
|
+
'教育背景', '教育经历', 'education',
|
|
50
|
+
'核心领导力', '重大经历', '资质与证书', '求职意向'
|
|
51
|
+
];
|
|
52
|
+
async function validateAts(htmlPath) {
|
|
53
|
+
const absoluteHtml = path.resolve(htmlPath);
|
|
54
|
+
if (!fs.existsSync(absoluteHtml)) {
|
|
55
|
+
throw new Error(`HTML file not found: ${absoluteHtml}`);
|
|
56
|
+
}
|
|
57
|
+
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
58
|
+
const page = await browser.newPage();
|
|
59
|
+
await page.goto(`file://${absoluteHtml}`, { waitUntil: 'networkidle' });
|
|
60
|
+
const rawData = await page.evaluate(() => {
|
|
61
|
+
const nameEl = document.querySelector('.candidate-name') || document.querySelector('h1');
|
|
62
|
+
const candidateName = nameEl ? nameEl.textContent?.trim() || null : null;
|
|
63
|
+
const bodyText = document.body.innerText || '';
|
|
64
|
+
const emailMatch = bodyText.match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/);
|
|
65
|
+
const phoneMatch = bodyText.match(/1[3-9]\d{9}|\+?\d{1,4}[-\s]?\d{7,11}/);
|
|
66
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4')).map(h => ({
|
|
67
|
+
level: h.tagName.toLowerCase(),
|
|
68
|
+
text: h.textContent?.replace(/[\s\n]+/g, ' ').trim() || ''
|
|
69
|
+
}));
|
|
70
|
+
return {
|
|
71
|
+
candidateName,
|
|
72
|
+
email: emailMatch ? emailMatch[0] : null,
|
|
73
|
+
phone: phoneMatch ? phoneMatch[0] : null,
|
|
74
|
+
headings,
|
|
75
|
+
totalTextLength: bodyText.trim().length
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
await browser.close();
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const errors = [];
|
|
81
|
+
if (!rawData.candidateName) {
|
|
82
|
+
errors.push('Candidate name (.candidate-name or h1) could not be extracted');
|
|
83
|
+
}
|
|
84
|
+
if (!rawData.email) {
|
|
85
|
+
warnings.push('No valid email address pattern detected in text flow');
|
|
86
|
+
}
|
|
87
|
+
if (!rawData.phone) {
|
|
88
|
+
warnings.push('No standard mobile phone pattern detected in text flow');
|
|
89
|
+
}
|
|
90
|
+
const validatedHeadings = rawData.headings.map(h => {
|
|
91
|
+
const textClean = h.text.toLowerCase();
|
|
92
|
+
const isStandard = STANDARD_SECTION_HEADERS.some(sh => textClean.includes(sh.toLowerCase()));
|
|
93
|
+
return {
|
|
94
|
+
...h,
|
|
95
|
+
isStandard
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const nonStdHeadings = validatedHeadings.filter(h => !h.isStandard && h.level === 'h2');
|
|
99
|
+
if (nonStdHeadings.length > 0) {
|
|
100
|
+
warnings.push(`Non-standard section headings detected: ${nonStdHeadings.map(h => `"${h.text}"`).join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
const status = errors.length > 0 ? 'FAIL' : warnings.length > 0 ? 'WARN' : 'PASS';
|
|
103
|
+
return {
|
|
104
|
+
status,
|
|
105
|
+
candidateName: rawData.candidateName,
|
|
106
|
+
detectedContacts: {
|
|
107
|
+
email: rawData.email,
|
|
108
|
+
phone: rawData.phone
|
|
109
|
+
},
|
|
110
|
+
headingsFound: validatedHeadings,
|
|
111
|
+
totalTextLength: rawData.totalTextLength,
|
|
112
|
+
warnings,
|
|
113
|
+
errors
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function main() {
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
let htmlPath = 'workspace/resume.html';
|
|
119
|
+
let jsonOutput = false;
|
|
120
|
+
for (let i = 0; i < args.length; i++) {
|
|
121
|
+
if (args[i] === '--html' || args[i] === '-i') {
|
|
122
|
+
htmlPath = args[i + 1];
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
else if (args[i] === '--json') {
|
|
126
|
+
jsonOutput = true;
|
|
127
|
+
}
|
|
128
|
+
else if (!args[i].startsWith('-')) {
|
|
129
|
+
htmlPath = args[i];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const result = await validateAts(htmlPath);
|
|
134
|
+
if (jsonOutput) {
|
|
135
|
+
console.log(JSON.stringify(result, null, 2));
|
|
136
|
+
process.exit(result.status === 'FAIL' ? 1 : 0);
|
|
137
|
+
}
|
|
138
|
+
console.log('======================================================');
|
|
139
|
+
console.log(` KnowMe CareerForge — ATS Compliance QA: [${result.status}]`);
|
|
140
|
+
console.log(` Candidate Name : ${result.candidateName || 'NOT FOUND'}`);
|
|
141
|
+
console.log(` Phone Extracted: ${result.detectedContacts.phone || 'NOT DETECTED'}`);
|
|
142
|
+
console.log(` Email Extracted: ${result.detectedContacts.email || 'NOT DETECTED'}`);
|
|
143
|
+
console.log(` Text Extracted : ${result.totalTextLength} characters`);
|
|
144
|
+
console.log('------------------------------------------------------');
|
|
145
|
+
console.log(' Headings Structure:');
|
|
146
|
+
for (const h of result.headingsFound) {
|
|
147
|
+
const mark = h.isStandard ? '[✓]' : '[?]';
|
|
148
|
+
console.log(` ${mark} <${h.level}> ${h.text}`);
|
|
149
|
+
}
|
|
150
|
+
if (result.warnings.length > 0) {
|
|
151
|
+
console.log('\n[!] Warnings:');
|
|
152
|
+
for (const w of result.warnings) {
|
|
153
|
+
console.log(` - ⚠️ ${w}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (result.errors.length > 0) {
|
|
157
|
+
console.log('\n[✗] Errors:');
|
|
158
|
+
for (const e of result.errors) {
|
|
159
|
+
console.log(` - ✗ ${e}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log('======================================================');
|
|
163
|
+
process.exit(result.status === 'FAIL' ? 1 : 0);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
167
|
+
console.error(`[✗] ATS Validation Failed: ${errorMsg}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
main();
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
/**
|
|
3
|
+
* KnowMe CareerForge — Playwright DOM Layout & Overflow Inspector
|
|
4
|
+
* 基于无头浏览器真实渲染计算 DOM 盒模型高度,检测单页/双页物理 A4 溢出与孤行。
|
|
5
|
+
*/
|
|
6
|
+
interface LayoutValidationResult {
|
|
7
|
+
status: 'PASS' | 'OVERFLOW';
|
|
8
|
+
expectedPages: number;
|
|
9
|
+
totalPagesFound: number;
|
|
10
|
+
pageDetails: Array<{
|
|
11
|
+
pageIndex: number;
|
|
12
|
+
actualHeightPx: number;
|
|
13
|
+
allowedHeightPx: number;
|
|
14
|
+
isOverflow: boolean;
|
|
15
|
+
overflowDeltaPx: number;
|
|
16
|
+
}>;
|
|
17
|
+
overflowSections: Array<{
|
|
18
|
+
selector: string;
|
|
19
|
+
bottomPx: number;
|
|
20
|
+
overflowByPx: number;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export declare function validateLayout(htmlPath: string, expectedPages?: number): Promise<LayoutValidationResult>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* KnowMe CareerForge — Playwright DOM Layout & Overflow Inspector
|
|
5
|
+
* 基于无头浏览器真实渲染计算 DOM 盒模型高度,检测单页/双页物理 A4 溢出与孤行。
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.validateLayout = validateLayout;
|
|
42
|
+
const playwright_1 = require("playwright");
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
async function validateLayout(htmlPath, expectedPages = 1) {
|
|
46
|
+
const absoluteHtml = path.resolve(htmlPath);
|
|
47
|
+
if (!fs.existsSync(absoluteHtml)) {
|
|
48
|
+
throw new Error(`HTML file not found: ${absoluteHtml}`);
|
|
49
|
+
}
|
|
50
|
+
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
51
|
+
const context = await browser.newContext({
|
|
52
|
+
viewport: { width: 794, height: 1123 }, // A4 at 96 DPI
|
|
53
|
+
deviceScaleFactor: 1
|
|
54
|
+
});
|
|
55
|
+
const page = await context.newPage();
|
|
56
|
+
await page.goto(`file://${absoluteHtml}`, { waitUntil: 'networkidle' });
|
|
57
|
+
await page.evaluateHandle('document.fonts.ready');
|
|
58
|
+
// A4 标准高度 (297mm in pixels at 96 DPI ~= 1122.5px)
|
|
59
|
+
const A4_HEIGHT_PX = 1122.5;
|
|
60
|
+
const evaluation = await page.evaluate((allowedHeight) => {
|
|
61
|
+
const pages = Array.from(document.querySelectorAll('.resume-page'));
|
|
62
|
+
const details = pages.map((p, idx) => {
|
|
63
|
+
const rect = p.getBoundingClientRect();
|
|
64
|
+
const actualHeight = Math.ceil(rect.height);
|
|
65
|
+
const isOverflow = actualHeight > allowedHeight + 2; // 2px 浮点容差
|
|
66
|
+
return {
|
|
67
|
+
pageIndex: idx + 1,
|
|
68
|
+
actualHeightPx: actualHeight,
|
|
69
|
+
allowedHeightPx: allowedHeight,
|
|
70
|
+
isOverflow,
|
|
71
|
+
overflowDeltaPx: isOverflow ? Math.ceil(actualHeight - allowedHeight) : 0
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
const overflowSections = [];
|
|
75
|
+
if (details.some(d => d.isOverflow)) {
|
|
76
|
+
const items = Array.from(document.querySelectorAll('.experience-item, .project-item, .skills-content, .resume-section'));
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const rect = item.getBoundingClientRect();
|
|
79
|
+
if (rect.bottom > allowedHeight) {
|
|
80
|
+
overflowSections.push({
|
|
81
|
+
selector: item.className ? `.${item.className.split(' ').join('.')}` : item.tagName.toLowerCase(),
|
|
82
|
+
bottomPx: Math.ceil(rect.bottom),
|
|
83
|
+
overflowByPx: Math.ceil(rect.bottom - allowedHeight)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
totalPagesFound: pages.length || 1,
|
|
90
|
+
pageDetails: details.length ? details : [{
|
|
91
|
+
pageIndex: 1,
|
|
92
|
+
actualHeightPx: Math.ceil(document.body.getBoundingClientRect().height),
|
|
93
|
+
allowedHeightPx: allowedHeight,
|
|
94
|
+
isOverflow: document.body.getBoundingClientRect().height > allowedHeight + 2,
|
|
95
|
+
overflowDeltaPx: Math.max(0, Math.ceil(document.body.getBoundingClientRect().height - allowedHeight))
|
|
96
|
+
}],
|
|
97
|
+
overflowSections
|
|
98
|
+
};
|
|
99
|
+
}, A4_HEIGHT_PX);
|
|
100
|
+
await browser.close();
|
|
101
|
+
const hasOverflow = evaluation.pageDetails.some(p => p.isOverflow) || evaluation.totalPagesFound > expectedPages;
|
|
102
|
+
return {
|
|
103
|
+
status: hasOverflow ? 'OVERFLOW' : 'PASS',
|
|
104
|
+
expectedPages,
|
|
105
|
+
totalPagesFound: evaluation.totalPagesFound,
|
|
106
|
+
pageDetails: evaluation.pageDetails,
|
|
107
|
+
overflowSections: evaluation.overflowSections
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
let htmlPath = 'workspace/resume.html';
|
|
113
|
+
let expectedPages = 1;
|
|
114
|
+
let jsonOutput = false;
|
|
115
|
+
for (let i = 0; i < args.length; i++) {
|
|
116
|
+
if (args[i] === '--html' || args[i] === '-i') {
|
|
117
|
+
htmlPath = args[i + 1];
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
else if (args[i] === '--expected-pages' || args[i] === '-p') {
|
|
121
|
+
expectedPages = parseInt(args[i + 1], 10) || 1;
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
else if (args[i] === '--json') {
|
|
125
|
+
jsonOutput = true;
|
|
126
|
+
}
|
|
127
|
+
else if (!args[i].startsWith('-')) {
|
|
128
|
+
htmlPath = args[i];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const result = await validateLayout(htmlPath, expectedPages);
|
|
133
|
+
if (jsonOutput) {
|
|
134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
135
|
+
process.exit(result.status === 'PASS' ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
console.log('======================================================');
|
|
138
|
+
console.log(` KnowMe CareerForge — Layout QA: ${result.status}`);
|
|
139
|
+
console.log(` HTML Path : ${htmlPath}`);
|
|
140
|
+
console.log(` Expected Pages : ${result.expectedPages} | Pages Found: ${result.totalPagesFound}`);
|
|
141
|
+
console.log('------------------------------------------------------');
|
|
142
|
+
for (const p of result.pageDetails) {
|
|
143
|
+
const fitText = p.isOverflow ? `[✗ OVERFLOW by ${p.overflowDeltaPx}px]` : `[✓ FIT] (Overflow: ${p.overflowDeltaPx}px)`;
|
|
144
|
+
console.log(` Page ${p.pageIndex}: ${p.actualHeightPx}px / ${p.allowedHeightPx}px ${fitText}`);
|
|
145
|
+
}
|
|
146
|
+
if (result.overflowSections.length > 0) {
|
|
147
|
+
console.log('\n[!] Offending Overflow Elements:');
|
|
148
|
+
for (const el of result.overflowSections.slice(0, 3)) {
|
|
149
|
+
console.log(` - Element "${el.selector}" exceeds A4 baseline by ${el.overflowByPx}px (Bottom: ${el.bottomPx}px)`);
|
|
150
|
+
}
|
|
151
|
+
console.log('\n💡 Recommendation: Reduce --resume-space-section by 1~2pt or reduce --resume-font-size-body by 0.2pt in <style>.');
|
|
152
|
+
}
|
|
153
|
+
console.log('======================================================');
|
|
154
|
+
process.exit(result.status === 'PASS' ? 0 : 1);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
158
|
+
console.error(`[✗] Layout Validation Failed: ${errorMsg}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
main();
|
|
164
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knowme-careerforge-skill",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "An agent-native skill for self-discovery, career positioning, and tailored resume engineering.",
|
|
5
5
|
"main": "dist/cli/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,16 +17,20 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
|
-
"test": "python3 scripts/run-all-tests.py",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
20
|
+
"test": "python3 scripts/build/run-all-tests.py",
|
|
21
|
+
"forge": "python3 scripts/pipeline/forge.py",
|
|
22
|
+
"extract": "python3 scripts/evidence/extract-evidence.py",
|
|
23
|
+
"search": "python3 scripts/template/search-template.py",
|
|
24
|
+
"analyze:jd": "python3 scripts/evidence/analyze-jd.py",
|
|
25
|
+
"instantiate": "python3 scripts/template/instantiate-resume.py",
|
|
26
|
+
"validate": "python3 scripts/validation/validate-resume.py",
|
|
27
|
+
"validate:layout": "ts-node scripts/validation/validate-layout.ts",
|
|
28
|
+
"validate:ats": "ts-node scripts/validation/validate-ats.ts",
|
|
29
|
+
"render:pdf": "python3 scripts/rendering/render-pdf.py",
|
|
30
|
+
"build:knowledge": "python3 scripts/build/build-knowledge.py",
|
|
31
|
+
"build:gallery": "python3 scripts/build/build-gallery.py",
|
|
32
|
+
"release": "python3 scripts/build/release.py",
|
|
33
|
+
"version:sync": "python3 scripts/build/sync-version.py",
|
|
30
34
|
"prepublishOnly": "npm run build && npm test"
|
|
31
35
|
},
|
|
32
36
|
"keywords": [
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"homepage": "https://github.com/Max-Samson/knowme-careerforge-skill",
|
|
45
49
|
"repository": {
|
|
46
50
|
"type": "git",
|
|
47
|
-
"url": "https://github.com/Max-Samson/knowme-careerforge-skill.git"
|
|
51
|
+
"url": "git+https://github.com/Max-Samson/knowme-careerforge-skill.git"
|
|
48
52
|
},
|
|
49
53
|
"bugs": {
|
|
50
54
|
"url": "https://github.com/Max-Samson/knowme-careerforge-skill/issues"
|