knowme-careerforge-skill 0.0.6 → 0.0.8

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 (75) hide show
  1. package/AGENT.md +7 -6
  2. package/ARCHITECTURE.md +21 -14
  3. package/README.md +36 -25
  4. package/README.zh-CN.md +36 -25
  5. package/SKILL.md +42 -27
  6. package/agents/claude/knowme-careerforge.md +1 -1
  7. package/agents/codex/knowme-careerforge.yaml +2 -18
  8. package/agents/cursor/knowme-careerforge.mdc +1 -1
  9. package/agents/gemini/knowme-careerforge.json +1 -1
  10. package/agents/opencode/skill.yaml +1 -1
  11. package/agents/windsurf/knowme-careerforge.rules +1 -1
  12. package/dist/cli/src/commands/init.js +51 -32
  13. package/dist/cli/src/index.js +22 -14
  14. package/package.json +1 -1
  15. package/references/01-evidence-mining.md +110 -3
  16. package/references/02-career-goal.md +36 -14
  17. package/references/03-jd-analysis.md +13 -15
  18. package/references/04-template-selection.md +27 -35
  19. package/references/05-html-canvas-tokens.md +4 -2
  20. package/references/06-qa-and-rendering.md +50 -3
  21. package/references/07-artifact-contract.md +39 -6
  22. package/scripts/Agent.md +3 -3
  23. package/scripts/build/build-gallery.py +2 -1
  24. package/scripts/build/release.py +3 -2
  25. package/scripts/evidence/analyze-jd.py +49 -8
  26. package/scripts/evidence/extract-evidence.py +17 -391
  27. package/scripts/pipeline/forge.py +165 -81
  28. package/scripts/rendering/browser-engine.js +47 -1
  29. package/scripts/template/instantiate-resume.py +5 -3
  30. package/scripts/template/search-template.py +9 -0
  31. package/skill.json +7 -6
  32. package/src/knowledge/career/seniority-signals.json +1 -0
  33. package/src/knowledge/index.json +176 -2
  34. package/src/knowledge/resume/action-verbs.json +3 -2
  35. package/src/knowledge/resume/fab-model.json +5 -4
  36. package/src/knowledge/resume-schema.json +1 -1
  37. package/src/knowledge/templates.json +174 -0
  38. package/src/references/01-evidence-mining.md +110 -3
  39. package/src/references/02-career-goal.md +36 -14
  40. package/src/references/03-jd-analysis.md +13 -15
  41. package/src/references/04-template-selection.md +27 -35
  42. package/src/references/05-html-canvas-tokens.md +4 -2
  43. package/src/references/06-qa-and-rendering.md +50 -3
  44. package/src/references/07-artifact-contract.md +39 -6
  45. package/src/templates/academic-research/canvas.html +2 -1
  46. package/src/templates/classic/canvas.html +2 -1
  47. package/src/templates/classic/style.css +90 -28
  48. package/src/templates/common/base.css +19 -3
  49. package/src/templates/common/canvas-bindings.css +248 -13
  50. package/src/templates/compact-dense/style.css +5 -1
  51. package/src/templates/creative-tech/style.css +30 -1
  52. package/src/templates/data-analyst/style.css +31 -1
  53. package/src/templates/editorial/README.md +7 -0
  54. package/src/templates/editorial/canvas.html +22 -0
  55. package/src/templates/editorial/metadata.json +36 -0
  56. package/src/templates/editorial/sample-profile.json +86 -0
  57. package/src/templates/editorial/style.css +24 -0
  58. package/src/templates/executive/style.css +52 -18
  59. package/src/templates/graduate/README.md +7 -0
  60. package/src/templates/graduate/canvas.html +22 -0
  61. package/src/templates/graduate/metadata.json +38 -0
  62. package/src/templates/graduate/sample-profile.json +60 -0
  63. package/src/templates/graduate/style.css +24 -0
  64. package/src/templates/international-flow/canvas.html +2 -1
  65. package/src/templates/minimal/README.md +3 -1
  66. package/src/templates/minimal/canvas.html +3 -2
  67. package/src/templates/minimal/style.css +6 -0
  68. package/src/templates/modern/style.css +43 -55
  69. package/src/templates/project-focus/README.md +7 -0
  70. package/src/templates/project-focus/canvas.html +22 -0
  71. package/src/templates/project-focus/metadata.json +36 -0
  72. package/src/templates/project-focus/sample-profile.json +93 -0
  73. package/src/templates/project-focus/style.css +24 -0
  74. package/src/templates/startup-generalist/canvas.html +2 -1
  75. package/src/templates/startup-generalist/style.css +15 -8
@@ -1,14 +1,61 @@
1
- # QA and final rendering
1
+ # PDF generation, QA and final rendering
2
2
 
3
3
  Validation and rendering use scripts/rendering/browser-engine.js. Inspect print media after fonts settle, every page container and visible content boundary; generate a fresh PDF and parse every page for A4 portrait dimensions, allowed page count and extractable content matching the canvas.
4
4
 
5
5
  PASS requires the actual checks to complete. Missing browser/dependencies or timeout is UNVERIFIED (exit 2), a failed check is FAIL (exit 1). JSON mode uses the same exit rules. Do not use an estimate or an existing PDF as proof. Export verified bytes through a temporary file and atomic replacement; failed export retains the previous file.
6
6
 
7
- The pipeline isolates each run and records artifact hashes in manifest.json. Use its current PASS output paths, not shared default filenames. Factual and visual review remain Agent responsibilities; text extraction does not certify every ATS. See 07-artifact-contract.md for the complete lifecycle.
7
+ The pipeline isolates each run and returns accepted artifact hashes. Default staging is temporary; only explicit --keep-workspace/--workspace retains a manifest. Use this invocation’s PASS output paths, not the existence of a shared default filename. A null manifest in clean mode is expected. Factual and visual review remain Agent responsibilities; text extraction does not certify every ATS. See 07-artifact-contract.md for the complete lifecycle.
8
8
 
9
9
 
10
10
  ## 本地字体与缺字诊断
11
11
 
12
- `forge.py` 和 `instantiate-resume.py` 支持 `--font-preset system|arial-unicode`,默认保留系统字体。`arial-unicode` 显式使用本机 Arial Unicode MS;没有该字体时字体验收失败并返回 UNVERIFIED,不保证跨机器可用,也不自动下载字体。字体选择只改变画布样式,不改变 Master 事实,并记录在 Variant 的 source.fontPreset 和运行清单中。
12
+ `forge.py --profile-json` 和 `instantiate-resume.py` 支持 `--font-preset system|arial-unicode`,默认保留系统字体。`arial-unicode` 显式使用本机 Arial Unicode MS;没有该字体时字体验收失败并返回 UNVERIFIED,不保证跨机器可用,也不自动下载字体。字体选择只改变画布样式,不改变 Master 事实,并记录在 Variant 的 source.fontPreset 和运行清单中。
13
13
 
14
14
  中文 PDF 若文本验收失败,检查 QA 每页的 missingCodepoints、missingFragments 与 extractedRadicals。部首字符可能来自字体字码映射差异,也可能存在真实缺字;不能仅凭这种提示放行。可选用本机可用字体、创建新运行并重新检查,不改写姓名/经历来绕过问题,也不把部首字符全部忽略。
15
+
16
+ ## What each check can actually establish
17
+
18
+ | Evidence | What it supports | What it cannot establish |
19
+ | --- | --- | --- |
20
+ | Original user material compared with rewritten claims | Fidelity to supplied facts | Independent verification of the user's history |
21
+ | Master/Variant hashes and bound text | Run provenance and rendering consistency | That Agent-written profile claims came from the user |
22
+ | Current PDF page/text/layout checks | Technical PDF acceptance | Complete optional information, ideal design or universal ATS support |
23
+ | Every page of the final PDF viewed | Actual visual appearance of those pages | Authenticity of the candidate's achievements |
24
+
25
+ Keep PDF inspection images and helper scripts in system temporary storage, clean your own temporary files afterward, and leave only the delivery pair in the output directory. Inspect the PDF itself using the host's PDF viewer or an available PDF-to-image tool, not a screen-media HTML preview. If no such tool is available, say the PDF's visual review is incomplete; keep the technical PASS distinction. Do not install a second rendering system just to repeat checks the pipeline already performed.
26
+
27
+ When a font mapping warning occurs, read the failure report’s diagnostics (or archived QA when retention was requested) for missing fragments/codepoints and compare with the source text and page appearance. A warning is a diagnostic clue, not a diagnosis or an automatic reason to force a specific font. Use an available explicit font only as a justified retry; preserve facts and recheck the new PDF. After the current export and review pass, deliver rather than creating more copies or rerunning unchanged checks.
28
+
29
+ ## Production commands (HTML/PDF requests only)
30
+
31
+ Use the absolute SKILL.md path already returned by the host to resolve `<skill-root>`. The model name does not determine the installation directory: a Gemini model may be running through a host that resolves `.claude/skills`. If unavailable, inspect the host's configured skill location; do not recursively search the home directory. Candidate files belong in the user's workspace, never the installation directory.
32
+
33
+ After there is sufficient candidate material, and before the first PDF build, run:
34
+
35
+ ```bash
36
+ node <skill-root>/scripts/rendering/browser-engine.js --check-runtime
37
+ ```
38
+
39
+ READY only checks runtime availability, not resume quality. If unavailable, use its diagnostics and the installed package.json to prepare dependencies once under the host's permission rules. With a lockfile use `npm ci --omit=dev`; otherwise use `npm install --omit=dev`, in `<skill-root>`. Do not guess and install packages one error at a time. If package.json or required bundled scripts are absent, the installation is incomplete: repair it from the project package rather than synthesizing a manifest or installing guessed dependencies. Chromium is also required. If setup is blocked, report UNVERIFIED and offer a requested Draft; do not repeatedly run the full pipeline unchanged.
40
+
41
+ For a new resume, pass the prepared JSON through stdin. Default clean mode leaves only `resume/resume.html` and `resume/resume.pdf` (or explicit output paths). Do not create profile, analysis, QA, process or history files unless the user requests them:
42
+
43
+ ```bash
44
+ python3 <skill-root>/scripts/pipeline/forge.py --profile-json - --role "<target role>" --template <chosen-id>
45
+ ```
46
+
47
+ Send the JSON with the command tool's stdin support or `subprocess.run(..., input=json.dumps(profile, ensure_ascii=False), text=True)`; do not interpolate candidate text into a shell command. If the host requires a file, use its temporary-file facility and remove only that newly created input after the command; never delete supplied files.
48
+
49
+ Choose one layout before building. Do not render all search candidates for comparison unless requested. If an actual visual defect requires a different template, fix the cause and replace the same delivery pair rather than creating named versions. A selected accepted result is already a deliverable: do not rerun merely to obtain another copy.
50
+
51
+ Drafts use `--draft` and return only `resume/draft.html` by default; they never update the current PDF. Read the exact returned status and paths. A successful preflight need not be repeated on contact or wording updates unless the environment changes. Clean mode reports `manifest: null`: it is intentional, not a missing-file error. Do not search for a run directory or reconstruct deleted diagnostics.
52
+
53
+ For updates, read the current HTML, apply only the requested changes **in memory**, then submit the full edited HTML to the same pipeline through stdin:
54
+
55
+ ```bash
56
+ python3 <skill-root>/scripts/pipeline/forge.py --html-input -
57
+ ```
58
+
59
+ Use explicit output paths if the current pair is elsewhere. Do not overwrite the current HTML before validation: the pipeline stages the edit and publishes both files only after acceptance, preserving the old pair on ordinary failures. The report labels this `existing-canvas`, meaning the supplied structure/fonts are preserved, not that a new template was selected. Do not rebuild a profile from rendered text or create backup/version files to make a contact edit.
60
+
61
+ Use returned diagnostics first; retry only after changing the identified cause. Only when the user requests archives or a specific unresolved failure requires full diagnostic retention, add `--keep-workspace`. An explicit `--workspace <dir>` also retains runs for compatibility with archived workflows. These options deliberately create extra files and are not part of normal resume production. Default temporary intermediates are cleaned on success and failure; existing user files and old archives are never swept or deleted.
@@ -23,7 +23,7 @@
23
23
 
24
24
  `profile` 的字段定义见 `src/knowledge/resume-schema.json`;脚本 `scripts/contracts/profile.py` 是归一化与封装契约的实现。脚本输出时附加 `profileSha256` 和 `missingFields`。摘要用于发现资料版本变化,不证明用户陈述真实性。手动修改封装资料后,应重新生成摘要或移除旧摘要再读入;不能保留不匹配的摘要。
25
25
 
26
- 兼容旧版的裸 profile JSON,按调用者明确提供的 Master 事实输入处理。封装为 Draft 的输入必须加 `--draft` 才能生成草稿;复核后由 Agent 显式整理为 Master,不由工具自动升级。Variant 的语义改写与事实选择仍由宿主 Agent 完成;当前脚本保留所有输入条目,只处理显式基础信息覆盖、岗位标题及展示装配。
26
+ 兼容旧版的裸 profile JSON,按调用者明确提供的 Master 事实输入处理。封装为 Draft 的输入必须加 `--draft` 才能生成草稿;复核后由 Agent 显式整理为 Master,不由工具自动升级。Variant 的语义改写与事实选择仍由宿主 Agent 完成;当前脚本保留所有输入条目,只处理显式基础信息覆盖、岗位标题及展示装配。Agent 应先根据用户目标组织、扩写和选择本次展示内容,再提交 profile;此时 Master 是本次提交内容的事实快照,不代表保存了用户全部原始材料。原始对话或用户文件仍是核对依据,不从该快照的省略推断用户没有其他经历。完整事实库与显式选择清单尚未实现,不宣称管线自动完成岗位定制。
27
27
 
28
28
  ## 缺失值
29
29
 
@@ -33,13 +33,27 @@
33
33
  - 缺失联系方式和目标岗位记录为待补信息。正式交付至少需要非空姓名及实质内容,打印与 PDF 验收进一步判断内容有效性。
34
34
  - 旧版 L1/L2/L3 元数据可保留;用户陈述不因为来自对话就自动降级为“参与”。
35
35
 
36
- ## 每次运行独立
36
+ ## 独立运行与产物保留
37
37
 
38
- `forge --profile-json <file>` 在 `workspace/runs/<唯一运行ID>/` 下创建私有目录,包含 `input.json` 原始输入快照、`master.json`、`variant.json`、`resume.html`、`qa.json`、通过验收后写入的 `resume.pdf` 和 `manifest.json`。Draft 运行保存 `draft.json` 和草稿画布,不生成正式 PDF。
38
+ 运行隔离不等于永久保存每次运行。默认模式在系统私有临时目录中完成输入快照、Master/Variant、模板装配及完整 QA;正常成功或失败后清理本次临时目录。默认不会在用户目录创建 `workspace/`、隐藏事实档案或历史快照。
39
39
 
40
- `--workspace` 仅改变运行目录的父目录,不复用已有运行。失败现场保留供诊断,不自动删除或继续使用其他运行的文件。目录包含个人资料,不提交到 Git;可由用户按需删除。测试必须使用临时目录。
40
+ | 调用模式 | 用户目录新增产物 | 终端结果 |
41
+ | --- | --- | --- |
42
+ | 默认 `--profile-json -` | `resume/resume.html`、`resume/resume.pdf` | 简短报告,`retention: temporary`、`manifest: null`,仅当前交付路径及摘要 |
43
+ | 默认 `--draft` | `resume/draft.html` | DRAFT,仅 HTML,不修改已有 PDF |
44
+ | `--html-input -` | 验收后的当前 HTML/PDF 对 | 复用同一 QA 与发布机制,`templateUsed: existing-canvas`,不重建事实档案 |
45
+ | `--keep-workspace` | `workspace/runs/<runId>/` 完整归档 | `retention: archived`,默认完整报告;可用 `--summary` |
46
+ | 显式 `--workspace <dir>` | 指定父目录下的完整归档 | 保留原有归档行为,等同显式开启保留 |
47
+
48
+ `--profile-json` 接受已有文件或 `-`(从 stdin 读取 JSON)。`--html-input` 接受已有画布或 `-`(从 stdin 读取 HTML),与 profile 输入互斥;不能同时传岗位、模板、字体或基础字段覆盖参数,相关修改应先在画布中完成。文件输入只读;输出路径不得与输入文件冲突。为了更新同一份简历,读取当前 HTML 并在内存中修改后通过 stdin 提交,避免提前破坏当前交付。
49
+
50
+ 默认输出可由 `--output`、`--html-output` 显式指定;只指定一项时,另一项使用相同主文件名与对应扩展名。归档模式不指定输出副本时,交付路径仍在归档目录。Draft 支持 `--html-output`,拒绝 PDF `--output`。
51
+
52
+ 只有完成本次完整验收的 HTML/PDF 才一起准备发布;普通复制或替换失败回滚,目标锁阻止两个本工具进程交错写入。跨文件发布不具备操作系统崩溃下的事务保证。默认不读取旧文件判定成功,也不创建 `latest.json` 等额外成功标记;当前调用状态、输出路径及摘要是交付依据。不要擅自删除遗留锁。
41
53
 
42
- `--html-output` 和 `--output` 是可选的已验收副本路径。先在独立运行中完成验证,再一起准备副本;写入失败时回滚普通错误,目标锁阻止两个本工具进程交错写入。跨文件复制不具备操作系统崩溃下的事务保证,因此运行目录和 manifest 始终是交付判据。发现遗留锁时报告冲突,不擅自删除其他运行的锁。
54
+ 归档模式保存原有输入、Master/Variant 或 Draft、HTML、QA、manifest 和通过验收的 PDF;HTML 输入不生成虚假的 Master/Variant,保存输入 HTML 与检查记录。失败现场仅在归档模式保留。纯净模式返回错误和必要诊断(含文本失败的缺字片段/码点、DOM 失败的页码/选择器/边界及 PDF 页数/尺寸;DOM 每类问题最多返回 20 条并附总数,不输出完整 DOM 正文),不返回已删除临时文件作为产物;需要深入诊断时显式保留下一次运行。进程被强制终止可能留下系统临时文件,不承诺强制终止时自动清理。
55
+
56
+ 清理范围仅为本次工具创建的临时目录,不删除用户输入、旧版 `workspace/`、用户指定的记录文件或已有交付文件。目录中已有内容不会因升级而被“清空”。资料属于用户隐私,不提交到 Git。
43
57
 
44
58
  ## 运行与检查状态
45
59
 
@@ -51,7 +65,7 @@
51
65
  | FAIL | 1 | 输入、绑定、布局、PDF 内容或输出写入不符合要求 |
52
66
  | UNVERIFIED | 2 | 浏览器、依赖、超时或检查协议故障,无法完成验收 |
53
67
 
54
- `manifest.json` 包含 `runId`、`stage`、`errors`、`warnings`、`checks`、`outputs`。失败的 `outputs` 为空,即使目录或显式输出位置存在旧 PDF,也不能返回它作为本次交付。只有 PASS 的清单记录本次输出路径及 HTML/PDF SHA-256。JSON 模式与人读模式使用同一退出语义。
68
+ 归档模式的 `manifest.json` 包含 `runId`、`stage`、`errors`、`warnings`、`checks`、`outputs`。失败的 `outputs` 为空,即使目录或显式输出位置存在旧 PDF,也不能返回它作为本次交付。PASS 报告记录本次 HTML/PDF 输出路径及 SHA-256;DRAFT 可记录 HTML 路径与摘要,但不表示 PDF 通过。JSON 模式与人读模式使用同一退出语义。
55
69
 
56
70
  ## 打印与最终 PDF
57
71
 
@@ -62,3 +76,22 @@
62
76
  当前分页契约要求每个 `.resume-page` 对应一个 A4 页面,`--expected-pages` 为 1 或 2 的上限。一页文档可用上限 2;两页文档需由 Agent 显式建立两个页面容器。单个超高容器自然流入多页会被拒绝,不猜测浏览器分页后每页应有的内容。
63
77
 
64
78
  输入文档的来源说明保存在 Master 的 source.suppliedSource 中,包括 synthetic-simulation 等原始标签。它是来源元数据,不是新增候选人事实。字体预设单独记录为 Variant.source.fontPreset;不得把字体修订误记为事实变更。
79
+
80
+ ## 简短输入与交付入口
81
+
82
+ Agent 可直接整理裸 profile;只填写用户提供的值,删除未知字段及不需要的章节。以下尖括号仅说明字段含义,不能作为候选人数据执行:
83
+
84
+ ```json
85
+ {
86
+ "basics": {"name": "<用户姓名>", "title": "<目标岗位>"},
87
+ "skills": [{"category": "<分组>", "items": ["<用户提供的技能>"]}],
88
+ "experience": [{"company": "<公司>", "role": "<实际职务>",
89
+ "startDate": "<开始时间>", "endDate": "<结束时间>",
90
+ "bullets": [{"text": "<用户提供的职责或成果>"}]}],
91
+ "education": [{"degree": "<学历>", "field": "<专业>"}]
92
+ }
93
+ ```
94
+
95
+ 姓名和至少一个实质章节是当前 PDF 输入门槛;联系方式、学校、专业和日期允许未知。`missingFields` 仅列出 basics.name/title/email/phone 的缺失,空列表不表示所有字段完整,更不证明事实真实。
96
+
97
+ 默认纯净模式已经返回简短报告;`--summary` 在归档模式下精简终端报告。`--quiet` 只控制 JSON 格式,不决定是否留存文件。需要完整历史的旧调用应显式传 `--workspace` 或 `--keep-workspace`;没有这些参数的调用改为纯净输出,不再默认保存完整报告。失败的 outputs 始终为空,不能把路径下残留的旧 PDF 当成本次成功。
package/scripts/Agent.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Script responsibilities
2
2
 
3
3
  - contracts/: shared Draft/Master/Variant normalization and missing-value rules.
4
- - pipeline/: unique run directory, immutable input snapshot, Master and Variant lineage, explicit result manifest and verified copies.
5
- - evidence/: optional JD helpers; legacy extract-evidence.py is not a supported candidate input path.
4
+ - pipeline/: isolated temporary staging by default, explicit retained archives, immutable input snapshots, Master/Variant lineage, and atomic verified delivery pairs.
5
+ - evidence/: optional JD helpers; legacy extract-evidence.py is a fail-only compatibility entry shared by CLI/npm/direct callers; it must not read repositories or write candidate profiles.
6
6
  - template/: template ranking and deterministic fact-only canvas binding.
7
7
  - validation/: print-mode geometry and content checks through the shared browser engine; no estimate-based PASS.
8
8
  - rendering/: final PDF page/text validation and atomic write, preserving old outputs on failure.
9
9
  - build/: package, gallery and test utilities. Tests must write only to temporary directories.
10
10
 
11
- Read ../references/07-artifact-contract.md for the authoritative artifact lifecycle, status/exit-code rules and output contract. Runtime scripts are resolved from the Skill installation; candidate artifacts belong to the user's workspace. A Draft is useful output but is never a verified PDF. FAIL and UNVERIFIED are nonzero exits in JSON and human-readable modes.
11
+ Read ../references/07-artifact-contract.md for the authoritative artifact lifecycle, status/exit-code rules and output contract. Pure output is a filesystem contract: stdin inputs need no permanent profile file, and default generation leaves only current delivery files. --keep-workspace or explicit --workspace opts into archive retention; never delete user input or existing archives. Runtime scripts are resolved from the Skill installation; candidate artifacts belong to the user's workspace. A Draft is useful output but is never a verified PDF. FAIL and UNVERIFIED are nonzero exits in JSON and human-readable modes.
@@ -226,11 +226,12 @@ def _render_gallery(gallery_dir, font_preset):
226
226
  item = {k: ([escape(str(v)) for v in value] if isinstance(value, list) else escape(str(value), quote=True))
227
227
  for k, value in raw_item.items()}
228
228
  tags_html = "".join([f'<span class="tag">{r}</span>' for r in item['supported'][:5]])
229
+ badge_cls = "badge optimal" if "optimal" in str(raw_item.get("atsTier", "")).lower() else "badge"
229
230
  gallery_html += f"""
230
231
  <div class="card">
231
232
  <div class="card-header">
232
233
  <span class="card-title">{item['name']}</span>
233
- <span class="badge">{item['atsTier']}</span>
234
+ <span class="{badge_cls}">{item['atsTier']}</span>
234
235
  </div>
235
236
  <div class="preview-frame-wrapper">
236
237
  <iframe class="preview-frame" src="{item['previewFile']}"></iframe>
@@ -28,8 +28,9 @@ def run_command(cmd, cwd, desc):
28
28
  def main():
29
29
  parser = argparse.ArgumentParser(description="KnowMe CareerForge — Release & NPM Publish Helper")
30
30
  parser.add_argument("version", nargs="?", help="New semantic version to release (e.g. 1.0.1, 1.1.0)")
31
- parser.add_argument("--dry-run", action="store_true", help="Perform build and test verification without publishing")
32
- parser.add_argument("--publish", action="store_true", help="Directly trigger npm publish --access public after verification")
31
+ mode = parser.add_mutually_exclusive_group()
32
+ mode.add_argument("--dry-run", action="store_true", help="Perform build and test verification without publishing")
33
+ mode.add_argument("--publish", action="store_true", help="Directly trigger npm publish --access public after verification")
33
34
 
34
35
  args = parser.parse_args()
35
36
  base_dir = get_project_root()
@@ -16,6 +16,53 @@ COMMON_TECH_STACKS = [
16
16
  "Docker", "Kubernetes", "K8s", "CI/CD", "AWS", "Aliyun", "Linux"
17
17
  ]
18
18
 
19
+ # ASCII identifier boundaries allow Chinese prose next to technology names.
20
+ SKILL_PATTERNS = {
21
+ skill: re.compile(rf'(?<![A-Za-z0-9_+]){re.escape(skill)}(?![A-Za-z0-9_+])', re.IGNORECASE)
22
+ for skill in COMMON_TECH_STACKS
23
+ }
24
+ REQUIREMENT_MARKERS = {
25
+ 'must': re.compile(r'必备(?:技能)?|必须|任职要求|岗位要求|基本要求|(?<![A-Za-z])(?:required|requirements|must[- ]have|mandatory)(?![A-Za-z])', re.I),
26
+ 'nice': re.compile(r'加分(?:项)?|优先|非必需|非必须|不要求|无需|(?<![A-Za-z])(?:nice[- ]to[- ]have|preferred|optional|not required)(?![A-Za-z])', re.I),
27
+ }
28
+
29
+
30
+ def classify_skills(jd_text, detected_skills):
31
+ """Classify only explicit requirements; ambiguous or conflicting mentions stay unclassified."""
32
+ evidence = {skill: set() for skill in detected_skills}
33
+ section = None
34
+ for line in jd_text.splitlines():
35
+ markdown_heading = bool(re.match(r'^\s*#{1,6}\s+', line))
36
+ line = line.strip().strip('#* ').strip()
37
+ if not line:
38
+ continue
39
+ heading = line.rstrip('::').strip()
40
+ kinds = [kind for kind, pattern in REQUIREMENT_MARKERS.items() if pattern.fullmatch(heading)]
41
+ if kinds:
42
+ section = kinds[0]
43
+ continue
44
+ # A different heading terminates an explicit requirements section.
45
+ if markdown_heading or (line.endswith((':', ':')) and not any(p.search(line) for p in SKILL_PATTERNS.values())):
46
+ section = None
47
+ continue
48
+ local_kind = section
49
+ for clause in re.split(r'([。;;,,!?!?]|\.(?=\s|$))', line):
50
+ if clause in ('。', ';', ';', '.', '!', '?', '!', '?'):
51
+ local_kind = section
52
+ continue
53
+ markers = {kind for kind, pattern in REQUIREMENT_MARKERS.items() if pattern.search(clause)}
54
+ # Negated requirements must not also count as positive "required".
55
+ if re.search(r'not required|非必须', clause, re.I):
56
+ markers.discard('must')
57
+ kind = next(iter(markers)) if len(markers) == 1 else local_kind if not markers else None
58
+ local_kind = kind
59
+ for skill in detected_skills:
60
+ if SKILL_PATTERNS[skill].search(clause) and kind:
61
+ evidence[skill].add(kind)
62
+ return ([s for s in detected_skills if evidence[s] == {'must'}],
63
+ [s for s in detected_skills if evidence[s] == {'nice'}])
64
+
65
+
19
66
  def analyze_jd_text(jd_text: str) -> dict:
20
67
  seniority = "mid"
21
68
  if any(k in jd_text for k in ["资深", "专家", "架构师", "总监", "Lead", "Senior", "Principal"]):
@@ -27,14 +74,8 @@ def analyze_jd_text(jd_text: str) -> dict:
27
74
  if any(k in jd_text for k in ["产品经理", "Product Manager", "总监", "Director", "业务负责人"]):
28
75
  category = "management-product"
29
76
 
30
- detected_skills = []
31
- for skill in COMMON_TECH_STACKS:
32
- pattern = re.compile(rf'(?:^|[^\w]){re.escape(skill)}(?:[^\w]|$)', re.IGNORECASE)
33
- if pattern.search(jd_text):
34
- detected_skills.append(skill)
35
-
36
- must_haves = detected_skills[:6] if len(detected_skills) >= 6 else detected_skills
37
- nice_to_haves = detected_skills[6:] if len(detected_skills) > 6 else []
77
+ detected_skills = [skill for skill, pattern in SKILL_PATTERNS.items() if pattern.search(jd_text)]
78
+ must_haves, nice_to_haves = classify_skills(jd_text, detected_skills)
38
79
 
39
80
  responsibilities = []
40
81
  for line in jd_text.splitlines():