knowme-careerforge-skill 0.0.4 → 0.0.7
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 +220 -0
- package/README.md +46 -63
- package/README.zh-CN.md +47 -66
- package/SKILL.md +38 -135
- package/agents/claude/knowme-careerforge.md +2 -17
- package/agents/codex/knowme-careerforge.yaml +2 -20
- package/agents/cursor/knowme-careerforge.mdc +4 -13
- package/agents/gemini/knowme-careerforge.json +1 -1
- package/agents/opencode/skill.yaml +1 -1
- package/agents/windsurf/knowme-careerforge.rules +2 -12
- package/dist/cli/src/commands/init.js +51 -32
- 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 +35 -0
- package/references/02-career-goal.md +40 -0
- package/references/03-jd-analysis.md +32 -0
- package/references/04-template-selection.md +30 -0
- package/references/05-html-canvas-tokens.md +12 -0
- package/references/06-qa-and-rendering.md +27 -0
- package/references/07-artifact-contract.md +83 -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 +276 -136
- package/scripts/rendering/README.md +65 -0
- package/scripts/rendering/browser-engine.d.ts +15 -0
- package/scripts/rendering/browser-engine.js +408 -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 +89 -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 +7 -6
- 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 +24 -30
- package/src/references/02-career-goal.md +4 -2
- package/src/references/03-jd-analysis.md +5 -17
- package/src/references/04-template-selection.md +20 -33
- package/src/references/05-html-canvas-tokens.md +6 -45
- package/src/references/06-qa-and-rendering.md +16 -43
- package/src/references/07-artifact-contract.md +83 -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 +12 -1
- 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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# 资料、运行与验收契约
|
|
2
|
+
|
|
3
|
+
## Draft / Master / Variant
|
|
4
|
+
|
|
5
|
+
这三个值表示资料用途,不是质量评分,也不表示已独立核实事实。
|
|
6
|
+
|
|
7
|
+
| kind | 用途 | 约束 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| draft | 用户资料尚不完整的草稿 | 可缺姓名及经历;仅生成草稿画布,不能报告 PDF 已验收 |
|
|
10
|
+
| master | 用户或 Agent 整理的完整事实集合 | 每次运行只保存新快照,不反写输入;完整指保留本次已知事实,不强制具备所有可选字段 |
|
|
11
|
+
| variant | 为目标岗位派生的展示资料 | 记录来源 Master 的 profileSha256;岗位标题只修改 Variant,不回写 Master;不能作为下一份 Master 隐式导入 |
|
|
12
|
+
|
|
13
|
+
标准文档形状为:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"schemaVersion": "1.0",
|
|
18
|
+
"kind": "master",
|
|
19
|
+
"profile": {"basics": {"name": "候选人姓名"}},
|
|
20
|
+
"source": {"type": "user-input"}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`profile` 的字段定义见 `src/knowledge/resume-schema.json`;脚本 `scripts/contracts/profile.py` 是归一化与封装契约的实现。脚本输出时附加 `profileSha256` 和 `missingFields`。摘要用于发现资料版本变化,不证明用户陈述真实性。手动修改封装资料后,应重新生成摘要或移除旧摘要再读入;不能保留不匹配的摘要。
|
|
25
|
+
|
|
26
|
+
兼容旧版的裸 profile JSON,按调用者明确提供的 Master 事实输入处理。封装为 Draft 的输入必须加 `--draft` 才能生成草稿;复核后由 Agent 显式整理为 Master,不由工具自动升级。Variant 的语义改写与事实选择仍由宿主 Agent 完成;当前脚本保留所有输入条目,只处理显式基础信息覆盖、岗位标题及展示装配。
|
|
27
|
+
|
|
28
|
+
## 缺失值
|
|
29
|
+
|
|
30
|
+
- 可选字段省略或 null 均表示未知,空白字符串归为 null,不生成占位候选人事实。
|
|
31
|
+
- null 的数组归为空数组;数组中的空白/null 项省略。未知字段和错误类型拒绝,不能悄悄丢失可能重要的资料。
|
|
32
|
+
- 教育经历、项目和工作经历为独立数组,按输入顺序完整处理;无教育描述不补“优秀毕业生”等默认荣誉。
|
|
33
|
+
- 缺失联系方式和目标岗位记录为待补信息。正式交付至少需要非空姓名及实质内容,打印与 PDF 验收进一步判断内容有效性。
|
|
34
|
+
- 旧版 L1/L2/L3 元数据可保留;用户陈述不因为来自对话就自动降级为“参与”。
|
|
35
|
+
|
|
36
|
+
## 每次运行独立
|
|
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。
|
|
39
|
+
|
|
40
|
+
`--workspace` 仅改变运行目录的父目录,不复用已有运行。失败现场保留供诊断,不自动删除或继续使用其他运行的文件。目录包含个人资料,不提交到 Git;可由用户按需删除。测试必须使用临时目录。
|
|
41
|
+
|
|
42
|
+
`--html-output` 和 `--output` 是可选的已验收副本路径。先在独立运行中完成验证,再一起准备副本;写入失败时回滚普通错误,目标锁阻止两个本工具进程交错写入。跨文件复制不具备操作系统崩溃下的事务保证,因此运行目录和 manifest 始终是交付判据。发现遗留锁时报告冲突,不擅自删除其他运行的锁。
|
|
43
|
+
|
|
44
|
+
## 运行与检查状态
|
|
45
|
+
|
|
46
|
+
| status | 退出码 | 意义 |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| RUNNING | 不作为最终结果 | 已开始但未完成,不能交付 |
|
|
49
|
+
| DRAFT | 0 | 草稿准备完成,无 PDF 验收承诺 |
|
|
50
|
+
| PASS | 0 | 本次指定检查及最终 PDF 验收通过 |
|
|
51
|
+
| FAIL | 1 | 输入、绑定、布局、PDF 内容或输出写入不符合要求 |
|
|
52
|
+
| UNVERIFIED | 2 | 浏览器、依赖、超时或检查协议故障,无法完成验收 |
|
|
53
|
+
|
|
54
|
+
`manifest.json` 包含 `runId`、`stage`、`errors`、`warnings`、`checks`、`outputs`。失败的 `outputs` 为空,即使目录或显式输出位置存在旧 PDF,也不能返回它作为本次交付。只有 PASS 的清单记录本次输出路径及 HTML/PDF SHA-256。JSON 模式与人读模式使用同一退出语义。
|
|
55
|
+
|
|
56
|
+
## 打印与最终 PDF
|
|
57
|
+
|
|
58
|
+
布局与导出共用 `scripts/rendering/browser-engine.js`,以 print 媒体模式等待字体,检查所有页面容器与内容边界。再生成新的 PDF,解析所有页,检查 A4 纵向尺寸、页数上限、逐页可提取文本与正文覆盖。最终写入的是通过验收的 PDF 字节,使用临时文件原子替换,不接受旧输出文件作为生成成功的证据。
|
|
59
|
+
|
|
60
|
+
自动调优不得增加当前间距,正文字号不降至 8.8pt 以下;调优失败保留原画布,不删改事实来强行过关。浏览器不可用不得用字数/盒模型估算替代 PASS。PDF 文本提取通过不代表所有 ATS 对多栏阅读顺序的解释一致;事实与视觉仍需 Agent 复核。
|
|
61
|
+
|
|
62
|
+
当前分页契约要求每个 `.resume-page` 对应一个 A4 页面,`--expected-pages` 为 1 或 2 的上限。一页文档可用上限 2;两页文档需由 Agent 显式建立两个页面容器。单个超高容器自然流入多页会被拒绝,不猜测浏览器分页后每页应有的内容。
|
|
63
|
+
|
|
64
|
+
输入文档的来源说明保存在 Master 的 source.suppliedSource 中,包括 synthetic-simulation 等原始标签。它是来源元数据,不是新增候选人事实。字体预设单独记录为 Variant.source.fontPreset;不得把字体修订误记为事实变更。
|
|
65
|
+
|
|
66
|
+
## 简短输入与交付入口
|
|
67
|
+
|
|
68
|
+
Agent 可直接整理裸 profile;只填写用户提供的值,删除未知字段及不需要的章节。以下尖括号仅说明字段含义,不能作为候选人数据执行:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"basics": {"name": "<用户姓名>", "title": "<目标岗位>"},
|
|
73
|
+
"skills": [{"category": "<分组>", "items": ["<用户提供的技能>"]}],
|
|
74
|
+
"experience": [{"company": "<公司>", "role": "<实际职务>",
|
|
75
|
+
"startDate": "<开始时间>", "endDate": "<结束时间>",
|
|
76
|
+
"bullets": [{"text": "<用户提供的职责或成果>"}]}],
|
|
77
|
+
"education": [{"degree": "<学历>", "field": "<专业>"}]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
姓名和至少一个实质章节是当前 PDF 输入门槛;联系方式、学校、专业和日期允许未知。`missingFields` 仅列出 basics.name/title/email/phone 的缺失,空列表不表示所有字段完整,更不证明事实真实。
|
|
82
|
+
|
|
83
|
+
`forge.py --summary` 仅精简终端报告,包含当前状态、模板、错误、仅 html/pdf 的 outputs 和 manifest 路径;默认及 `--quiet` 仍保留完整 JSON。完整 manifest 与 QA 始终落盘。正常交付用 `--output resume/resume.pdf --html-output resume/resume.html` 创建验收副本;内部运行历史不作为用户默认交付清单。失败时副本可能仍为旧版本,不能据此报告本次成功。Draft 不使用这些验收副本选项。
|
package/scripts/Agent.md
CHANGED
|
@@ -1,138 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Script responsibilities
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
6
|
+
- template/: template ranking and deterministic fact-only canvas binding.
|
|
7
|
+
- validation/: print-mode geometry and content checks through the shared browser engine; no estimate-based PASS.
|
|
8
|
+
- rendering/: final PDF page/text validation and atomic write, preserving old outputs on failure.
|
|
9
|
+
- build/: package, gallery and test utilities. Tests must write only to temporary directories.
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## 1. 目录架构与模块职责划分 (Domain Architecture)
|
|
11
|
-
|
|
12
|
-
为了保证脚本工具链的高内聚、低耦合与易维护性,`scripts/` 目录划分为 **6 大功能领域模块**:
|
|
13
|
-
|
|
14
|
-
```text
|
|
15
|
-
scripts/
|
|
16
|
-
├── Agent.md # 本规范文档 (Source of Truth for Agent/Dev)
|
|
17
|
-
│
|
|
18
|
-
├── pipeline/ # 【模块 1: 端到端统筹管线】
|
|
19
|
-
│ └── forge.py # 一键端到端装配引擎 (One-Shot Pipeline: 挖掘 -> 装配 -> QA -> PDF)
|
|
20
|
-
│
|
|
21
|
-
├── evidence/ # 【模块 2: 事实与证据挖掘 (repo-to-resume)】
|
|
22
|
-
│ ├── extract-evidence.py # 代码仓与 Git 事实挖掘器 (提取技术栈、架构信号、Git 贡献 -> L1~L3 证据链)
|
|
23
|
-
│ └── analyze-jd.py # 目标 JD 深度分析器 (提取职级、分类、必备/加分项、痛点关键词)
|
|
24
|
-
│
|
|
25
|
-
├── template/ # 【模块 3: 模板检索与工作区装配】
|
|
26
|
-
│ ├── search-template.py # 多维加权打分搜索引擎 (BM25 + 岗位/风格/页数/密度推荐)
|
|
27
|
-
│ └── instantiate-resume.py # 模板装配与数据注入引擎 (CSS 内联单文件 + 结构化数据注入 + 关键词高亮)
|
|
28
|
-
│
|
|
29
|
-
├── validation/ # 【模块 4: 双重 QA 自动化质检】
|
|
30
|
-
│ ├── validate-resume.py # 语义结构、Design Tokens 与字符密度轻量校验器
|
|
31
|
-
│ ├── validate-layout.ts # Playwright DOM 盒模型物理高度 (1122.5px A4) 溢出与孤行检测
|
|
32
|
-
│ └── validate-ats.ts # ATS 纯文本流解析、联系方式与 H1/H2 标头层级校验
|
|
33
|
-
│
|
|
34
|
-
├── rendering/ # 【模块 5: 确定性 PDF 渲染导出】
|
|
35
|
-
│ ├── render-pdf.py # 跨平台自愈式 PDF 渲染调度器 (自动探测 Playwright 及全平台 Chromium/Edge/Brave)
|
|
36
|
-
│ └── render-pdf.ts # Playwright 驱动的无损矢量 A4 PDF 渲染器
|
|
37
|
-
│
|
|
38
|
-
└── build/ # 【模块 6: 编译构建、测试与发布工具链】
|
|
39
|
-
├── build-knowledge.py # 岗位画像与模板知识库索引编译器 (生成 index.json & templates.json)
|
|
40
|
-
├── build-gallery.py # 静态交互式模板画廊生成器 (编译 output/templates_gallery/)
|
|
41
|
-
├── run-all-tests.py # 全链路自动化测试运行器 (discover & run tests/)
|
|
42
|
-
├── sync-version.py # 多清单版本号同步器 (package.json / pyproject.toml / skill.json / cli)
|
|
43
|
-
└── release.py # 自动化发布流水线引擎 (版本对齐 -> 编译 -> 测试 -> 打包预检)
|
|
44
|
-
```
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## 2. 脚本契约与 Agent 调用规范 (Execution Contracts)
|
|
48
|
-
|
|
49
|
-
### 2.1 推荐调用模式:一键直达管线 (One-Shot Execution)
|
|
50
|
-
|
|
51
|
-
AI Agent 响应用户求职定制需求时,**强烈推荐首选一键管线**:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# 静默全流程执行 (后台自动完成:代码仓扫描 -> 模板检索 -> 工作区注入 -> Dual QA -> PDF 导出)
|
|
55
|
-
python3 scripts/pipeline/forge.py --repo . --role "<Target Role>" --jd "<path/to/jd.txt>" --template modern --quiet
|
|
56
|
-
```
|
|
57
|
-
* **CLI 等价形式**:`knowme forge --repo . --role "<Target Role>" --template modern --quiet`
|
|
58
|
-
* **返回格式**:结构化 JSON,供 Agent 直接获取交付文件路径:
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"status": "SUCCESS",
|
|
62
|
-
"htmlCanvas": "workspace/resume.html",
|
|
63
|
-
"pdfDelivery": "workspace/resume.pdf",
|
|
64
|
-
"evidenceProfile": "workspace/evidence-master.json",
|
|
65
|
-
"templateUsed": "modern"
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
### 2.2 细粒度分步调用接口 (Granular Step-by-Step APIs)
|
|
72
|
-
|
|
73
|
-
当 Agent 或用户需要分步微调、审查证据或调校 Design Tokens 时,按以下顺序执行:
|
|
74
|
-
|
|
75
|
-
| 步骤 | 阶段 | 推荐命令 | 关键参数 | 产出物 |
|
|
76
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
77
|
-
| **Step 1** | **事实挖掘** | `python3 scripts/evidence/extract-evidence.py` | `--repo <dir>` `--name <name>` `--quiet` | `workspace/evidence-master.json` |
|
|
78
|
-
| **Step 2** | **JD 解析** | `python3 scripts/evidence/analyze-jd.py` | `--jd <file>` 或 `--text "<str>"` `--json` | 提取必须技能与权重关键词 |
|
|
79
|
-
| **Step 3** | **模板检索** | `python3 scripts/template/search-template.py` | `"<Role>"` `--style "<style>"` `--json` | 加权推荐模板列表 (Top 1) |
|
|
80
|
-
| **Step 4** | **画布装配** | `python3 scripts/template/instantiate-resume.py` | `--template <id>` `--profile <json>` `--keywords <kw>` | `workspace/resume.html` (单文件内联) |
|
|
81
|
-
| **Step 5** | **质检自愈** | `python3 scripts/validation/validate-resume.py` | `--html workspace/resume.html` `--expected-pages 1` | QA 诊断状态与溢出提示 |
|
|
82
|
-
| **Step 6** | **PDF 导出** | `python3 scripts/rendering/render-pdf.py` | `workspace/resume.html` `workspace/resume.pdf` `--quiet` | `workspace/resume.pdf` (确定性矢量) |
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## 3. Agent 对话降噪规范 (Strict Quiet Execution Protocol)
|
|
87
|
-
|
|
88
|
-
为了杜绝对话框中出现几百行冗余代码或中间脚本,所有接入的 AI Agent 必须遵守以下铁律:
|
|
89
|
-
|
|
90
|
-
1. **禁止在 Chat 中打印原始 HTML/CSS**:禁止将 `workspace/resume.html` 的数百行代码打印到对话框中;
|
|
91
|
-
2. **禁止在 Chat 中编写现场临时脚本**:严禁现场生成临时验证脚本或修改场脚本,必须使用 `scripts/` 下既有的标准工具链;
|
|
92
|
-
3. **静默执行原则**:调用脚本时统一添加 `--quiet` 或 `--json` 参数;
|
|
93
|
-
4. **标准交付物格式**:
|
|
94
|
-
```markdown
|
|
95
|
-
### 🎯 职业定位与核心价值主张 (Core Value Proposition)
|
|
96
|
-
> **[姓名] · [目标岗位]**
|
|
97
|
-
> *[15~25字核心定位与价值阐述]*
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
### 🛡️ Top 3 真实证据链亮点 (L1/L2 级别可追溯事实)
|
|
102
|
-
1. **[业务方向/核心架构]**: [动词 + 可量化成果 + 证据来源]
|
|
103
|
-
2. **[核心技术栈工程化]**: [关键框架与工程规范落地]
|
|
104
|
-
3. **[交付与系统效能]**: [稳定性/CI-CD/性能指标]
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
### 📄 最终交付产物
|
|
109
|
-
- **PDF 终稿 (确定性 A4)**: `workspace/resume.pdf`
|
|
110
|
-
- **HTML 修改场 (Design Tokens)**: `workspace/resume.html`
|
|
111
|
-
- **证据事实底座**: `workspace/evidence-master.json`
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## 4. 后续开发与扩展规范 (Developer Standards)
|
|
117
|
-
|
|
118
|
-
为保持工程整洁与跨平台稳定性,后续新增或修改脚本必须遵循以下标准:
|
|
119
|
-
|
|
120
|
-
### 4.1 根路径自适应解析 (Root Path Resolution Standard)
|
|
121
|
-
所有 Python 脚本**严禁使用硬编码相对路径**,必须通过自适应探测函数定位项目根目录:
|
|
122
|
-
```python
|
|
123
|
-
def get_project_root() -> Path:
|
|
124
|
-
curr = Path(__file__).resolve().parent
|
|
125
|
-
for _ in range(5):
|
|
126
|
-
if (curr / "SKILL.md").exists() or (curr / "package.json").exists():
|
|
127
|
-
return curr
|
|
128
|
-
curr = curr.parent
|
|
129
|
-
return Path.cwd()
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 4.2 零重型依赖原则 (Zero-Heavy-Dependency)
|
|
133
|
-
* 核心运行脚本(`evidence/`、`template/`、`pipeline/`)**仅使用 Python 3.9+ 标准库**(`json`, `argparse`, `pathlib`, `re`, `subprocess` 等);
|
|
134
|
-
* 避免引入大型三方包(如 pandas、scikit-learn),保证轻量秒开与跨平台零安装成本。
|
|
135
|
-
|
|
136
|
-
### 4.3 渲染与 QA 规范
|
|
137
|
-
* A4 绝对高度基准固化为 `1122.5px` ($297\text{mm} \times 96\text{DPI} / 25.4$);
|
|
138
|
-
* 任何涉及排版验证与 PDF 导出的改动,必须通过 `scripts/build/run-all-tests.py` 全量测试套件验证。
|
|
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.
|
|
@@ -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
|