knowme-careerforge-skill 0.0.4 → 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.
- package/AGENT.md +276 -0
- package/ARCHITECTURE.md +216 -0
- package/README.md +34 -54
- package/README.zh-CN.md +35 -57
- package/SKILL.md +28 -147
- package/agents/claude/knowme-careerforge.md +2 -17
- package/agents/codex/knowme-careerforge.yaml +3 -5
- package/agents/cursor/knowme-careerforge.mdc +4 -13
- package/agents/opencode/skill.yaml +1 -1
- package/agents/windsurf/knowme-careerforge.rules +2 -12
- package/dist/cli/src/index.js +25 -21
- package/dist/scripts/rendering/engine-loader.d.ts +2 -0
- package/dist/scripts/rendering/engine-loader.js +46 -0
- package/dist/scripts/rendering/render-pdf.d.ts +3 -8
- package/dist/scripts/rendering/render-pdf.js +7 -103
- package/dist/scripts/rendering/runtime.d.ts +9 -0
- package/dist/scripts/rendering/runtime.js +63 -0
- package/dist/scripts/validation/validate-ats.d.ts +2 -22
- package/dist/scripts/validation/validate-ats.js +6 -168
- package/dist/scripts/validation/validate-layout.d.ts +2 -23
- package/dist/scripts/validation/validate-layout.js +5 -159
- package/package.json +15 -4
- package/references/01-evidence-mining.md +5 -0
- package/references/02-career-goal.md +38 -0
- package/references/03-jd-analysis.md +44 -0
- package/references/04-template-selection.md +43 -0
- package/references/05-html-canvas-tokens.md +12 -0
- package/references/06-qa-and-rendering.md +14 -0
- package/references/07-artifact-contract.md +64 -0
- package/scripts/Agent.md +9 -136
- package/scripts/build/build-gallery.py +66 -56
- package/scripts/build/run-all-tests.py +1 -0
- package/scripts/build/sync-version.py +27 -9
- package/scripts/contracts/profile.py +101 -0
- package/scripts/evidence/extract-evidence.py +109 -62
- package/scripts/pipeline/forge.py +266 -136
- package/scripts/rendering/README.md +65 -0
- package/scripts/rendering/browser-engine.d.ts +15 -0
- package/scripts/rendering/browser-engine.js +366 -0
- package/scripts/rendering/browser_engine.py +39 -0
- package/scripts/rendering/engine-loader.ts +11 -0
- package/scripts/rendering/render-pdf.py +26 -151
- package/scripts/rendering/render-pdf.ts +7 -83
- package/scripts/template/instantiate-resume.py +266 -173
- package/scripts/template/search-template.py +80 -3
- package/scripts/validation/measure-dom.js +5 -0
- package/scripts/validation/validate-ats.ts +6 -174
- package/scripts/validation/validate-layout.ts +5 -159
- package/scripts/validation/validate-resume.py +23 -134
- package/skill.json +1 -1
- package/src/knowledge/index.json +391 -3
- package/src/knowledge/resume-schema.json +377 -71
- package/src/knowledge/templates.json +389 -1
- package/src/references/01-evidence-mining.md +3 -39
- package/src/references/05-html-canvas-tokens.md +6 -45
- package/src/references/06-qa-and-rendering.md +7 -47
- package/src/references/07-artifact-contract.md +64 -0
- package/src/templates/Agent.md +11 -0
- package/src/templates/academic-research/README.md +25 -0
- package/src/templates/academic-research/canvas.html +21 -0
- package/src/templates/academic-research/metadata.json +38 -0
- package/src/templates/academic-research/sample-profile.json +86 -0
- package/src/templates/academic-research/style.css +213 -0
- package/src/templates/classic/README.md +9 -0
- package/src/templates/classic/canvas.html +21 -0
- package/src/templates/classic/metadata.json +3 -0
- package/src/templates/classic/sample-profile.json +86 -0
- package/src/templates/classic/style.css +14 -4
- package/src/templates/common/base.css +32 -0
- package/src/templates/common/canvas-bindings.css +41 -0
- package/src/templates/common/resume-contract.md +8 -0
- package/src/templates/compact-dense/README.md +25 -0
- package/src/templates/compact-dense/canvas.html +14 -0
- package/src/templates/compact-dense/metadata.json +38 -0
- package/src/templates/compact-dense/sample-profile.json +86 -0
- package/src/templates/compact-dense/style.css +218 -0
- package/src/templates/creative-tech/README.md +26 -0
- package/src/templates/creative-tech/canvas.html +15 -0
- package/src/templates/creative-tech/metadata.json +39 -0
- package/src/templates/creative-tech/sample-profile.json +86 -0
- package/src/templates/creative-tech/style.css +257 -0
- package/src/templates/data-analyst/README.md +26 -0
- package/src/templates/data-analyst/canvas.html +15 -0
- package/src/templates/data-analyst/metadata.json +39 -0
- package/src/templates/data-analyst/sample-profile.json +86 -0
- package/src/templates/data-analyst/style.css +285 -0
- package/src/templates/executive/README.md +9 -0
- package/src/templates/executive/canvas.html +15 -0
- package/src/templates/executive/sample-profile.json +86 -0
- package/src/templates/executive/style.css +58 -6
- package/src/templates/international-flow/README.md +26 -0
- package/src/templates/international-flow/canvas.html +21 -0
- package/src/templates/international-flow/metadata.json +37 -0
- package/src/templates/international-flow/sample-profile.json +86 -0
- package/src/templates/international-flow/style.css +208 -0
- package/src/templates/minimal/README.md +9 -0
- package/src/templates/minimal/canvas.html +21 -0
- package/src/templates/minimal/sample-profile.json +86 -0
- package/src/templates/minimal/style.css +24 -0
- package/src/templates/modern/README.md +9 -0
- package/src/templates/modern/canvas.html +15 -0
- package/src/templates/modern/sample-profile.json +86 -0
- package/src/templates/modern/style.css +48 -1
- package/src/templates/startup-generalist/README.md +25 -0
- package/src/templates/startup-generalist/canvas.html +21 -0
- package/src/templates/startup-generalist/metadata.json +37 -0
- package/src/templates/startup-generalist/sample-profile.json +86 -0
- package/src/templates/startup-generalist/style.css +237 -0
- package/src/templates/classic/template.html +0 -133
- package/src/templates/executive/template.html +0 -101
- package/src/templates/minimal/template.html +0 -114
- package/src/templates/modern/template.html +0 -139
|
@@ -4,7 +4,13 @@ KnowMe CareerForge — Template Gallery Builder
|
|
|
4
4
|
扫描验证 src/templates/ 核心模板,生成 output/templates_gallery/ 静态可视化画廊。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import argparse
|
|
8
|
+
import importlib.util
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from html import escape
|
|
8
14
|
from pathlib import Path
|
|
9
15
|
|
|
10
16
|
def get_project_root() -> Path:
|
|
@@ -15,61 +21,55 @@ def get_project_root() -> Path:
|
|
|
15
21
|
curr = curr.parent
|
|
16
22
|
return Path.cwd()
|
|
17
23
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
def load_instantiator():
|
|
25
|
+
spec = importlib.util.spec_from_file_location(
|
|
26
|
+
"gallery_instantiator", get_project_root() / "scripts/template/instantiate-resume.py")
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_gallery(output_dir=None, font_preset="system"):
|
|
33
|
+
"""Validate and stage every preview before publishing; publish the index last."""
|
|
34
|
+
gallery_dir = Path(output_dir) if output_dir else get_project_root() / "output/templates_gallery"
|
|
22
35
|
gallery_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
with tempfile.TemporaryDirectory(prefix=".gallery-", dir=gallery_dir) as scratch:
|
|
37
|
+
stage = Path(scratch)
|
|
38
|
+
_render_gallery(stage, font_preset)
|
|
39
|
+
for preview in sorted(stage.glob("*.html")):
|
|
40
|
+
if preview.name != "index.html":
|
|
41
|
+
os.replace(preview, gallery_dir / preview.name)
|
|
42
|
+
os.replace(stage / "index.html", gallery_dir / "index.html")
|
|
43
|
+
return gallery_dir / "index.html"
|
|
25
44
|
|
|
26
|
-
print("==============================================================")
|
|
27
|
-
print(" KnowMe CareerForge — Template Gallery Builder")
|
|
28
|
-
print("==============================================================")
|
|
29
45
|
|
|
46
|
+
def _render_gallery(gallery_dir, font_preset):
|
|
47
|
+
templates_dir = get_project_root() / "src/templates"
|
|
48
|
+
binder = load_instantiator()
|
|
49
|
+
gallery_items = []
|
|
30
50
|
for t_dir in sorted(templates_dir.iterdir()):
|
|
31
51
|
if not t_dir.is_dir() or t_dir.name == "common":
|
|
32
52
|
continue
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
meta
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
preview_path = gallery_dir / f"{t_dir.name}.html"
|
|
54
|
-
preview_path.write_text(inlined_preview, encoding="utf-8")
|
|
55
|
-
|
|
56
|
-
gallery_items.append({
|
|
57
|
-
"id": meta.get("id", t_dir.name),
|
|
58
|
-
"name": meta.get("name", t_dir.name),
|
|
59
|
-
"style": meta.get("style", "single-column"),
|
|
60
|
-
"category": meta.get("roleCategory", "engineering-ai"),
|
|
61
|
-
"tone": meta.get("visualStyle", {}).get("tone", "modern"),
|
|
62
|
-
"accent": meta.get("visualStyle", {}).get("accentColor", "#2563eb"),
|
|
63
|
-
"pages": meta.get("layout", {}).get("targetPages", 1),
|
|
64
|
-
"density": meta.get("layout", {}).get("density", "balanced"),
|
|
65
|
-
"atsTier": meta.get("atsScoreTier", "tier-1-optimal"),
|
|
66
|
-
"previewFile": f"{t_dir.name}.html",
|
|
67
|
-
"supported": meta.get("supportedRoles", [])
|
|
68
|
-
})
|
|
69
|
-
print(f"[✓] Generated Gallery Preview -> {preview_path.name}")
|
|
70
|
-
except Exception as e:
|
|
71
|
-
print(f"[✗] Error processing {t_dir.name}: {e}")
|
|
72
|
-
|
|
53
|
+
for name in ("canvas.html", "sample-profile.json", "style.css", "metadata.json"):
|
|
54
|
+
if not (t_dir / name).is_file():
|
|
55
|
+
raise ValueError(f"Incomplete template {t_dir.name}: missing {name}")
|
|
56
|
+
meta = json.loads((t_dir / "metadata.json").read_text(encoding="utf-8"))
|
|
57
|
+
binder.instantiate_workspace(
|
|
58
|
+
t_dir.name, profile_path=t_dir / "sample-profile.json",
|
|
59
|
+
output_path=gallery_dir / f"{t_dir.name}.html", quiet=True,
|
|
60
|
+
font_preset=font_preset)
|
|
61
|
+
gallery_items.append({
|
|
62
|
+
"name": meta.get("name", t_dir.name),
|
|
63
|
+
"style": meta.get("style", "single-column"),
|
|
64
|
+
"tone": meta.get("visualStyle", {}).get("tone", "modern"),
|
|
65
|
+
"pages": meta.get("layout", {}).get("targetPages", 1),
|
|
66
|
+
"density": meta.get("layout", {}).get("density", "balanced"),
|
|
67
|
+
"atsTier": meta.get("atsScoreTier", "unspecified"),
|
|
68
|
+
"previewFile": f"{t_dir.name}.html",
|
|
69
|
+
"supported": meta.get("supportedRoles", [])
|
|
70
|
+
})
|
|
71
|
+
if not gallery_items:
|
|
72
|
+
raise ValueError("No templates found")
|
|
73
73
|
# 生成主索引 index.html
|
|
74
74
|
gallery_html = f"""<!DOCTYPE html>
|
|
75
75
|
<html lang="zh-CN">
|
|
@@ -217,18 +217,20 @@ def build_gallery():
|
|
|
217
217
|
<div class="gallery-container">
|
|
218
218
|
<header>
|
|
219
219
|
<h1>KnowMe CareerForge — Template Gallery</h1>
|
|
220
|
-
<p class="subtitle"
|
|
220
|
+
<p class="subtitle">虚构样例 · 与实际简历共用 canvas.html 和完整样式 · ATS 标签仅代表设计目标</p>
|
|
221
221
|
</header>
|
|
222
222
|
<div class="grid">
|
|
223
223
|
"""
|
|
224
224
|
|
|
225
|
-
for
|
|
225
|
+
for raw_item in gallery_items:
|
|
226
|
+
item = {k: ([escape(str(v)) for v in value] if isinstance(value, list) else escape(str(value), quote=True))
|
|
227
|
+
for k, value in raw_item.items()}
|
|
226
228
|
tags_html = "".join([f'<span class="tag">{r}</span>' for r in item['supported'][:5]])
|
|
227
229
|
gallery_html += f"""
|
|
228
230
|
<div class="card">
|
|
229
231
|
<div class="card-header">
|
|
230
232
|
<span class="card-title">{item['name']}</span>
|
|
231
|
-
<span class="badge
|
|
233
|
+
<span class="badge">{item['atsTier']}</span>
|
|
232
234
|
</div>
|
|
233
235
|
<div class="preview-frame-wrapper">
|
|
234
236
|
<iframe class="preview-frame" src="{item['previewFile']}"></iframe>
|
|
@@ -255,8 +257,16 @@ def build_gallery():
|
|
|
255
257
|
"""
|
|
256
258
|
gallery_index = gallery_dir / "index.html"
|
|
257
259
|
gallery_index.write_text(gallery_html, encoding="utf-8")
|
|
258
|
-
|
|
259
|
-
print("==============================================================")
|
|
260
|
+
|
|
260
261
|
|
|
261
262
|
if __name__ == "__main__":
|
|
262
|
-
|
|
263
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
264
|
+
parser.add_argument("--output", type=Path, help="Gallery output directory")
|
|
265
|
+
parser.add_argument("--font-preset", choices=("system", "arial-unicode"), default="system")
|
|
266
|
+
args = parser.parse_args()
|
|
267
|
+
try:
|
|
268
|
+
index = build_gallery(args.output, args.font_preset)
|
|
269
|
+
print(json.dumps({"status": "PASS", "index": str(index)}, ensure_ascii=False))
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
print(json.dumps({"status": "FAIL", "error": str(exc)}, ensure_ascii=False), file=sys.stderr)
|
|
272
|
+
sys.exit(1)
|
|
@@ -19,6 +19,7 @@ def run_all_test_suites():
|
|
|
19
19
|
start_time = time.time()
|
|
20
20
|
base_dir = get_project_root()
|
|
21
21
|
tests_dir = base_dir / "tests"
|
|
22
|
+
sys.path.insert(0, str(base_dir))
|
|
22
23
|
|
|
23
24
|
print("==============================================================")
|
|
24
25
|
print(" KnowMe CareerForge — Full-Chain Automated Test Suite")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
KnowMe CareerForge — Version Synchronization Engine
|
|
4
|
-
将项目根版本号统一同步至 package.json, pyproject.toml, skill.json, cli/package.json, SKILL.md, agents/*,
|
|
4
|
+
将项目根版本号统一同步至 package.json, pyproject.toml, skill.json, cli/package.json, npm 锁文件, SKILL.md, agents/*, CLI 与知识索引。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import sys, os, json, re, argparse
|
|
@@ -18,7 +18,11 @@ def get_project_root() -> Path:
|
|
|
18
18
|
def sync_version(new_version: str):
|
|
19
19
|
base_dir = get_project_root()
|
|
20
20
|
# 去除可能传入的 'v' 前缀
|
|
21
|
-
semver = new_version.
|
|
21
|
+
semver = new_version.removeprefix("v")
|
|
22
|
+
core = r"(?:0|[1-9][0-9]*)"
|
|
23
|
+
prerelease = r"(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
|
|
24
|
+
if not re.fullmatch(rf"{core}\.{core}\.{core}(?:-{prerelease}(?:\.{prerelease})*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?", semver):
|
|
25
|
+
raise ValueError(f"Invalid semantic version: {new_version}")
|
|
22
26
|
|
|
23
27
|
print("==============================================================")
|
|
24
28
|
print(f" KnowMe CareerForge — Synchronizing Version to v{semver}")
|
|
@@ -40,6 +44,17 @@ def sync_version(new_version: str):
|
|
|
40
44
|
cli_pkg_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
41
45
|
print(f"[✓] Synced cli/package.json -> {semver}")
|
|
42
46
|
|
|
47
|
+
# Lockfiles carry the package version separately from dependency versions.
|
|
48
|
+
for relative in ("package-lock.json", "cli/package-lock.json"):
|
|
49
|
+
lock_path = base_dir / relative
|
|
50
|
+
if lock_path.exists():
|
|
51
|
+
data = json.loads(lock_path.read_text(encoding="utf-8"))
|
|
52
|
+
data["version"] = semver
|
|
53
|
+
if "" in data.get("packages", {}):
|
|
54
|
+
data["packages"][""]["version"] = semver
|
|
55
|
+
lock_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
56
|
+
print(f"[✓] Synced {relative} -> {semver}")
|
|
57
|
+
|
|
43
58
|
# 3. Update skill.json
|
|
44
59
|
skill_json_path = base_dir / "skill.json"
|
|
45
60
|
if skill_json_path.exists():
|
|
@@ -60,7 +75,7 @@ def sync_version(new_version: str):
|
|
|
60
75
|
skill_md_path = base_dir / "SKILL.md"
|
|
61
76
|
if skill_md_path.exists():
|
|
62
77
|
text = skill_md_path.read_text(encoding="utf-8")
|
|
63
|
-
new_text = re.sub(r'
|
|
78
|
+
new_text = re.sub(r'(?m)^(\s*version:)[ \t]*[^\r\n]+$', lambda m: f'{m[1]} {semver}', text)
|
|
64
79
|
skill_md_path.write_text(new_text, encoding="utf-8")
|
|
65
80
|
print(f"[✓] Synced SKILL.md -> {semver}")
|
|
66
81
|
|
|
@@ -68,7 +83,7 @@ def sync_version(new_version: str):
|
|
|
68
83
|
codex_yaml = base_dir / "agents" / "codex" / "knowme-careerforge.yaml"
|
|
69
84
|
if codex_yaml.exists():
|
|
70
85
|
text = codex_yaml.read_text(encoding="utf-8")
|
|
71
|
-
new_text = re.sub(r'
|
|
86
|
+
new_text = re.sub(r'(?m)^(\s*version:)[ \t]*[^\r\n]+$', lambda m: f'{m[1]} {semver}', text)
|
|
72
87
|
codex_yaml.write_text(new_text, encoding="utf-8")
|
|
73
88
|
print(f"[✓] Synced agents/codex/knowme-careerforge.yaml -> {semver}")
|
|
74
89
|
|
|
@@ -76,7 +91,7 @@ def sync_version(new_version: str):
|
|
|
76
91
|
opencode_yaml = base_dir / "agents" / "opencode" / "skill.yaml"
|
|
77
92
|
if opencode_yaml.exists():
|
|
78
93
|
text = opencode_yaml.read_text(encoding="utf-8")
|
|
79
|
-
new_text = re.sub(r'
|
|
94
|
+
new_text = re.sub(r'(?m)^(\s*version:)[ \t]*[^\r\n]+$', lambda m: f'{m[1]} {semver}', text)
|
|
80
95
|
opencode_yaml.write_text(new_text, encoding="utf-8")
|
|
81
96
|
print(f"[✓] Synced agents/opencode/skill.yaml -> {semver}")
|
|
82
97
|
# 8. Update cli/src/index.ts fallback version
|
|
@@ -84,7 +99,7 @@ def sync_version(new_version: str):
|
|
|
84
99
|
if cli_ts_path.exists():
|
|
85
100
|
text = cli_ts_path.read_text(encoding="utf-8")
|
|
86
101
|
new_text = re.sub(r"return\s+data\.version\s*\|\|\s*'[^']+'", f"return data.version || '{semver}'", text)
|
|
87
|
-
new_text = re.sub(r"return\s+'[^']
|
|
102
|
+
new_text = re.sub(r"return\s+'\d+\.\d+\.\d+[^']*'\s*;", f"return '{semver}';", new_text)
|
|
88
103
|
cli_ts_path.write_text(new_text, encoding="utf-8")
|
|
89
104
|
print(f"[✓] Synced cli/src/index.ts -> {semver}")
|
|
90
105
|
|
|
@@ -101,16 +116,19 @@ def sync_version(new_version: str):
|
|
|
101
116
|
kb_script = base_dir / "scripts" / "build" / "build-knowledge.py"
|
|
102
117
|
if kb_script.exists():
|
|
103
118
|
import subprocess
|
|
104
|
-
subprocess.run([sys.executable, str(kb_script)], stdout=subprocess.DEVNULL)
|
|
119
|
+
subprocess.run([sys.executable, str(kb_script)], stdout=subprocess.DEVNULL, check=True)
|
|
105
120
|
print(f"[✓] Recompiled knowledge index -> v{semver}")
|
|
106
121
|
|
|
107
122
|
print("\n[✓] All package versions successfully synchronized!")
|
|
108
123
|
|
|
109
124
|
def main():
|
|
110
125
|
parser = argparse.ArgumentParser(description="Synchronize project version")
|
|
111
|
-
parser.add_argument("version",
|
|
126
|
+
parser.add_argument("version", help="Required target semantic version (e.g. 0.0.6)")
|
|
112
127
|
args = parser.parse_args()
|
|
113
|
-
|
|
128
|
+
try:
|
|
129
|
+
sync_version(args.version)
|
|
130
|
+
except (ValueError, OSError) as error:
|
|
131
|
+
parser.exit(1, f"Version synchronization failed: {error}\n")
|
|
114
132
|
|
|
115
133
|
if __name__ == "__main__":
|
|
116
134
|
main()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared input contract. Structural validity does not certify candidate facts."""
|
|
2
|
+
import copy
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
8
|
+
SCHEMA = json.loads((ROOT / 'src/knowledge/resume-schema.json').read_text(encoding='utf-8'))
|
|
9
|
+
|
|
10
|
+
class ContractError(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _normalize(value, schema, path):
|
|
15
|
+
types = schema.get('type', [])
|
|
16
|
+
types = [types] if isinstance(types, str) else types
|
|
17
|
+
if value is None:
|
|
18
|
+
if 'null' not in types:
|
|
19
|
+
raise ContractError(path + ': null is not allowed')
|
|
20
|
+
return [] if 'array' in types else {} if 'object' in types else None
|
|
21
|
+
if isinstance(value, str):
|
|
22
|
+
if 'string' not in types:
|
|
23
|
+
raise ContractError(path + ': expected ' + '/'.join(types))
|
|
24
|
+
value = value.strip() or None
|
|
25
|
+
if value is not None and 'enum' in schema and value not in schema['enum']:
|
|
26
|
+
raise ContractError(path + ': invalid value')
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, dict) and 'object' in types:
|
|
29
|
+
properties = schema.get('properties', {})
|
|
30
|
+
unknown = set(value) - set(properties)
|
|
31
|
+
if unknown:
|
|
32
|
+
raise ContractError(path + ': unsupported fields: ' + ', '.join(sorted(unknown)))
|
|
33
|
+
result = {}
|
|
34
|
+
for key, child in properties.items():
|
|
35
|
+
if key in value:
|
|
36
|
+
item = _normalize(value[key], child, path + '.' + key)
|
|
37
|
+
if item is not None and item != {} and item != []:
|
|
38
|
+
result[key] = item
|
|
39
|
+
return result
|
|
40
|
+
if isinstance(value, list) and 'array' in types:
|
|
41
|
+
values = [_normalize(item, schema['items'], path + '[' + str(i) + ']') for i, item in enumerate(value)]
|
|
42
|
+
return [item for item in values if item is not None and item != '' and item != {}]
|
|
43
|
+
raise ContractError(path + ': expected ' + '/'.join(types))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_profile(raw):
|
|
47
|
+
result = _normalize(raw, SCHEMA, 'profile')
|
|
48
|
+
result.setdefault('basics', {})
|
|
49
|
+
for field in ('skills', 'experience', 'projects', 'education', 'certifications'):
|
|
50
|
+
result.setdefault(field, [])
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def digest(value):
|
|
55
|
+
return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def missing_fields(profile):
|
|
59
|
+
# Informational gaps; optional omissions never turn into invented defaults.
|
|
60
|
+
return ['basics.' + field for field in ('name', 'title', 'email', 'phone')
|
|
61
|
+
if not profile.get('basics', {}).get(field)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def delivery_errors(profile):
|
|
65
|
+
errors = []
|
|
66
|
+
if not profile['basics'].get('name'):
|
|
67
|
+
errors.append('A non-empty candidate name is required for delivery')
|
|
68
|
+
if not any(profile.get(key) for key in ('experience', 'projects', 'education', 'skills', 'certifications')):
|
|
69
|
+
errors.append('At least one substantive resume section is required for delivery')
|
|
70
|
+
return errors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def document(kind, profile, source=None):
|
|
74
|
+
if kind not in ('draft', 'master', 'variant'):
|
|
75
|
+
raise ContractError('Unknown document kind: ' + str(kind))
|
|
76
|
+
profile = normalize_profile(profile)
|
|
77
|
+
return {'schemaVersion': '1.0', 'kind': kind, 'profile': profile,
|
|
78
|
+
'profileSha256': digest(profile), 'missingFields': missing_fields(profile),
|
|
79
|
+
'source': copy.deepcopy(source or {})}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_document(raw):
|
|
83
|
+
if not isinstance(raw, dict):
|
|
84
|
+
raise ContractError('Profile document must be an object')
|
|
85
|
+
if 'profile' not in raw:
|
|
86
|
+
# Existing --profile-json inputs are explicit user/Agent supplied facts.
|
|
87
|
+
return document('master', raw, {'type': 'legacy-user-input'})
|
|
88
|
+
unknown = set(raw) - {'schemaVersion', 'kind', 'profile', 'profileSha256', 'missingFields', 'source'}
|
|
89
|
+
if unknown:
|
|
90
|
+
raise ContractError('Unsupported document fields: ' + ', '.join(sorted(unknown)))
|
|
91
|
+
if raw.get('schemaVersion') != '1.0':
|
|
92
|
+
raise ContractError('Unsupported profile schemaVersion')
|
|
93
|
+
source = raw.get('source', {})
|
|
94
|
+
if not isinstance(source, dict):
|
|
95
|
+
raise ContractError('Document source must be an object')
|
|
96
|
+
result = document(raw.get('kind'), raw['profile'], source)
|
|
97
|
+
if raw.get('profileSha256') and result['profileSha256'] != raw['profileSha256']:
|
|
98
|
+
raise ContractError('Profile hash mismatch; regenerate metadata after editing facts')
|
|
99
|
+
if result['kind'] == 'variant' and not source.get('masterSha256'):
|
|
100
|
+
raise ContractError('Variant requires source.masterSha256')
|
|
101
|
+
return result
|
|
@@ -225,86 +225,133 @@ def scan_repo(repo_path: Path) -> Dict[str, Any]:
|
|
|
225
225
|
return facts
|
|
226
226
|
|
|
227
227
|
def build_master_profile(facts: Dict[str, Any], user_overrides: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
228
|
+
"""Build structured candidate profile from codebase facts + optional user overrides.
|
|
229
|
+
|
|
230
|
+
Placeholder discipline:
|
|
231
|
+
- repo_mode=True : facts dict is populated from real repo scan; safe to synthesize
|
|
232
|
+
experience/education skeletons because they'll be user-reviewed before use.
|
|
233
|
+
- repo_mode=False: only user_overrides carry real data. NEVER emit fabricated placeholder
|
|
234
|
+
strings (fake company, fake school, fake dates). Use null / empty list instead.
|
|
235
|
+
"""
|
|
228
236
|
overrides = user_overrides or {}
|
|
229
237
|
author = facts.get("author_info", {})
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
repo_mode: bool = bool(facts.get("detected_tech_stacks") or facts.get("evidence_items"))
|
|
239
|
+
|
|
240
|
+
# ── Basics ────────────────────────────────────────────────────────────────
|
|
241
|
+
name = overrides.get("name") or author.get("name") or (None if not repo_mode else "候选人")
|
|
242
|
+
title = overrides.get("title") or (None if not repo_mode else "资深工程师 / 技术专家")
|
|
243
|
+
email = overrides.get("email") or author.get("email") or (None if not repo_mode else "candidate@example.com")
|
|
244
|
+
phone = overrides.get("phone") or (None if not repo_mode else None)
|
|
245
|
+
github = overrides.get("github") or author.get("git_remote") or None
|
|
246
|
+
location = overrides.get("location") or (None if not repo_mode else None)
|
|
247
|
+
|
|
237
248
|
tech_stacks = facts.get("detected_tech_stacks", [])
|
|
238
|
-
languages
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
languages = list(facts.get("detected_languages", {}).keys())
|
|
250
|
+
|
|
251
|
+
# ── Skills ────────────────────────────────────────────────────────────────
|
|
252
|
+
all_tech = tech_stacks + [l for l in languages if l not in tech_stacks]
|
|
253
|
+
arch_signals = facts.get("architectural_signals", [])
|
|
254
|
+
skills: list = []
|
|
255
|
+
if all_tech:
|
|
256
|
+
skills.append({"category": "核心技术栈", "items": all_tech, "highlighted": all_tech[:4]})
|
|
257
|
+
if arch_signals:
|
|
258
|
+
skills.append({"category": "工程化与架构", "items": arch_signals, "highlighted": arch_signals[:2]})
|
|
259
|
+
|
|
260
|
+
# ── Experience ────────────────────────────────────────────────────────────
|
|
261
|
+
bullets = [
|
|
262
|
+
{
|
|
263
|
+
"text": item["claim"],
|
|
264
|
+
"evidenceLevel": item["evidenceLevel"],
|
|
265
|
+
"evidenceSource": item["evidenceSource"],
|
|
266
|
+
"keywords": item.get("keywords", [])
|
|
267
|
+
}
|
|
268
|
+
for item in facts.get("evidence_items", [])
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
if repo_mode and bullets:
|
|
272
|
+
# Repo mode: synthesize a single experience entry from code evidence.
|
|
273
|
+
# Company / dates are placeholders that the user MUST review.
|
|
274
|
+
experience = [
|
|
263
275
|
{
|
|
264
|
-
"company": overrides.get("company") or "
|
|
265
|
-
"role": title,
|
|
266
|
-
"startDate":
|
|
267
|
-
"endDate": "至今",
|
|
268
|
-
"location": "
|
|
276
|
+
"company": overrides.get("company") or "[请填写:公司名称]",
|
|
277
|
+
"role": title or "[请填写:职位名称]",
|
|
278
|
+
"startDate": overrides.get("startDate") or "[请填写:开始时间]",
|
|
279
|
+
"endDate": overrides.get("endDate") or "至今",
|
|
280
|
+
"location": location or "[请填写:工作地点]",
|
|
269
281
|
"summary": "负责核心产品研发与关键架构攻坚",
|
|
270
|
-
"bullets":
|
|
271
|
-
{
|
|
272
|
-
"text": item["claim"],
|
|
273
|
-
"evidenceLevel": item["evidenceLevel"],
|
|
274
|
-
"evidenceSource": item["evidenceSource"],
|
|
275
|
-
"keywords": item["keywords"]
|
|
276
|
-
}
|
|
277
|
-
for item in facts.get("evidence_items", [])
|
|
278
|
-
]
|
|
282
|
+
"bullets": bullets
|
|
279
283
|
}
|
|
280
|
-
]
|
|
281
|
-
|
|
284
|
+
]
|
|
285
|
+
elif overrides.get("experience"):
|
|
286
|
+
experience = overrides["experience"]
|
|
287
|
+
else:
|
|
288
|
+
experience = []
|
|
289
|
+
|
|
290
|
+
# ── Projects ──────────────────────────────────────────────────────────────
|
|
291
|
+
if repo_mode and tech_stacks:
|
|
292
|
+
projects = [
|
|
282
293
|
{
|
|
283
|
-
"name": facts.get("repo_name"
|
|
294
|
+
"name": facts.get("repo_name") or "[请填写:项目名称]",
|
|
284
295
|
"role": "核心负责人 / 主力开发者",
|
|
285
296
|
"techStack": tech_stacks[:5],
|
|
286
297
|
"repoUrl": github,
|
|
287
|
-
"startDate": "
|
|
298
|
+
"startDate": "[请填写:开始时间]",
|
|
288
299
|
"endDate": "至今",
|
|
289
300
|
"bullets": [
|
|
290
|
-
{
|
|
291
|
-
"text": item["claim"],
|
|
292
|
-
"evidenceLevel": item["evidenceLevel"],
|
|
293
|
-
"evidenceSource": item["evidenceSource"]
|
|
294
|
-
}
|
|
301
|
+
{"text": item["claim"], "evidenceLevel": item["evidenceLevel"], "evidenceSource": item["evidenceSource"]}
|
|
295
302
|
for item in facts.get("evidence_items", [])
|
|
296
303
|
]
|
|
297
304
|
}
|
|
298
|
-
]
|
|
299
|
-
|
|
305
|
+
]
|
|
306
|
+
elif overrides.get("projects"):
|
|
307
|
+
projects = overrides["projects"]
|
|
308
|
+
else:
|
|
309
|
+
projects = []
|
|
310
|
+
|
|
311
|
+
# ── Education ─────────────────────────────────────────────────────────────
|
|
312
|
+
if overrides.get("education"):
|
|
313
|
+
education = overrides["education"]
|
|
314
|
+
elif overrides.get("school") or overrides.get("degree"):
|
|
315
|
+
education = [
|
|
316
|
+
{
|
|
317
|
+
"institution": overrides.get("school") or "[请填写:学校名称]",
|
|
318
|
+
"degree": overrides.get("degree") or "[请填写:学历与专业]",
|
|
319
|
+
"startDate": overrides.get("edu_start") or "[请填写:入学时间]",
|
|
320
|
+
"endDate": overrides.get("edu_end") or "[请填写:毕业时间]"
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
elif repo_mode:
|
|
324
|
+
# Repo mode: emit a reminder skeleton so the user knows to fill it in.
|
|
325
|
+
education = [
|
|
300
326
|
{
|
|
301
|
-
"institution":
|
|
302
|
-
"degree":
|
|
303
|
-
"startDate": "
|
|
304
|
-
"endDate": "
|
|
327
|
+
"institution": "[请填写:学校名称]",
|
|
328
|
+
"degree": "[请填写:学历与专业]",
|
|
329
|
+
"startDate": "[请填写:入学时间]",
|
|
330
|
+
"endDate": "[请填写:毕业时间]"
|
|
305
331
|
}
|
|
306
332
|
]
|
|
307
|
-
|
|
333
|
+
else:
|
|
334
|
+
education = []
|
|
335
|
+
|
|
336
|
+
# ── Assemble ──────────────────────────────────────────────────────────────
|
|
337
|
+
basics: Dict[str, Any] = {}
|
|
338
|
+
if name: basics["name"] = name
|
|
339
|
+
if title: basics["title"] = title
|
|
340
|
+
if email: basics["email"] = email
|
|
341
|
+
if phone: basics["phone"] = phone
|
|
342
|
+
if location: basics["location"] = location
|
|
343
|
+
if github: basics["github"] = github
|
|
344
|
+
if all_tech:
|
|
345
|
+
basics["summary"] = (
|
|
346
|
+
f"拥有坚实的技术研发与工程化落地实战经验,"
|
|
347
|
+
f"主导过多个基于 {', '.join(all_tech[:3])} 的核心系统设计与交付。"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
profile: Dict[str, Any] = {"basics": basics}
|
|
351
|
+
if skills: profile["skills"] = skills
|
|
352
|
+
if experience: profile["experience"] = experience
|
|
353
|
+
if projects: profile["projects"] = projects
|
|
354
|
+
if education: profile["education"] = education
|
|
308
355
|
return profile
|
|
309
356
|
|
|
310
357
|
def main():
|