Spaces:
Runtime error
Runtime error
File size: 6,894 Bytes
ffd36e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | """Apply role_curation.yaml to onet_roles_raw.yaml.
Reads:
backend/seed_data/onet_roles_raw.yaml
backend/seed_data/role_curation.yaml
backend/seed_data/manual_skill_augmentation.yaml (for skills mentioned in keep but absent from raw)
Writes:
backend/seed_data/onet_roles_curated.yaml (final seed-ready file)
backend/seed_data/onet_roles_curation_log.md (per-role audit log of keeps + drops + reasons)
Logic:
For each role:
1. Take its keep list from role_curation.yaml.
2. For each keep entry, find it in raw skills OR in augmentation; if found nowhere, error.
3. Apply optional weight / is_mandatory / required_level overrides.
4. Anything in raw NOT in keep is DROPPED — record reason from drop_reasons map (or "no reason given").
5. Write curated YAML with same shape as raw, plus a curated_at timestamp at the top.
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
SEED_DIR = REPO_ROOT / "seed_data"
RAW_FILE = SEED_DIR / "onet_roles_raw.yaml"
CURATION_FILE = SEED_DIR / "role_curation.yaml"
AUGMENTATION_FILE = SEED_DIR / "manual_skill_augmentation.yaml"
OUTPUT_FILE = SEED_DIR / "onet_roles_curated.yaml"
LOG_FILE = SEED_DIR / "onet_roles_curation_log.md"
def find_skill(skills: list[dict], name: str) -> dict | None:
for s in skills:
if s["skill_name"] == name:
return s
return None
def synth_from_augmentation(role_name: str, skill_name: str, aug_doc: dict) -> dict | None:
"""If a keep entry isn't in raw, see if it lives in the augmentation YAML."""
role_aug = aug_doc.get("augmentation", {}).get(role_name, [])
for entry in role_aug:
if entry["skill"] == skill_name:
return {
"skill_name": skill_name,
"category": entry.get("category", "Tools"),
"source": "manual",
"source_soc": None,
"is_mandatory": entry["is_mandatory"],
"weight": entry["weight"],
"required_level": entry["required_level"],
}
return None
def main() -> int:
raw = yaml.safe_load(RAW_FILE.read_text(encoding="utf-8"))
curation = yaml.safe_load(CURATION_FILE.read_text(encoding="utf-8"))
augmentation = yaml.safe_load(AUGMENTATION_FILE.read_text(encoding="utf-8"))
out_roles: list[dict] = []
log_sections: list[str] = []
log_sections.append(
f"# Role Curation Log\n\n"
f"Generated {datetime.now(timezone.utc).isoformat()}Z by `scripts/curate_roles.py`\n\n"
f"This log shows, per role, which skills were kept and which were dropped (with reason).\n"
f"Source of truth for keep decisions: `seed_data/role_curation.yaml`. Edit there + re-run.\n\n"
)
fatal = False
for role in raw["roles"]:
name = role["name"]
rule = curation.get("curation", {}).get(name)
if not rule:
print(f"WARN: no curation rule for '{name}', keeping all skills as-is",
file=sys.stderr)
out_roles.append(role)
continue
keep_entries = rule.get("keep", [])
drop_reasons = rule.get("drop_reasons", {})
# Build the curated skill list, in the order specified by `keep`
kept: list[dict] = []
kept_names: set[str] = set()
for entry in keep_entries:
skill_name = entry["name"]
if skill_name in kept_names:
continue
base = find_skill(role["skills"], skill_name)
if base is None:
# Try augmentation fallback
base = synth_from_augmentation(name, skill_name, augmentation)
if base is None:
print(f"FATAL: keep '{skill_name}' for '{name}' not in raw nor augmentation",
file=sys.stderr)
fatal = True
continue
entry_dict = dict(base) # shallow copy
# Apply overrides if present
for k in ("is_mandatory", "weight", "required_level", "category"):
if k in entry:
entry_dict[k] = entry[k]
kept.append(entry_dict)
kept_names.add(skill_name)
# Anything in raw not kept is dropped — log reason
dropped: list[tuple[str, str]] = []
raw_names = {s["skill_name"] for s in role["skills"]}
for s in role["skills"]:
if s["skill_name"] not in kept_names:
reason = drop_reasons.get(s["skill_name"], "no reason given (please add to role_curation.yaml drop_reasons)")
dropped.append((s["skill_name"], reason))
# Sanity: any keep referenced a skill that is NOT in raw and NOT in augmentation? Already errored above.
# Also: drop_reasons referencing a skill that wasn't in raw — warn.
for orphan in set(drop_reasons.keys()) - raw_names:
print(f" WARN [{name}]: drop_reasons mentions '{orphan}' but it wasn't in raw",
file=sys.stderr)
out_roles.append({
"name": name,
"industry": role["industry"],
"description": role["description"],
"primary_soc": role["primary_soc"],
"all_socs": role["all_socs"],
"skills": kept,
})
log_sections.append(
f"## {name}\n\n"
f"**Kept ({len(kept)} skills):**\n\n"
+ "\n".join(
f"- `{s['skill_name']}` "
f"({'mandatory' if s['is_mandatory'] else 'optional'}, "
f"weight={s['weight']}, level={s['required_level']}, source={s['source']})"
for s in kept
)
+ f"\n\n**Dropped ({len(dropped)} skills):**\n\n"
+ ("\n".join(f"- `{name}` — {reason}" for name, reason in dropped) if dropped else "_none_")
+ "\n\n"
)
print(f" {name}: {len(kept)} kept, {len(dropped)} dropped")
if fatal:
print("\nABORT: see FATAL messages above. Fix role_curation.yaml and retry.",
file=sys.stderr)
return 1
OUTPUT_FILE.write_text(
f"# Curated role+skill seed for GapGuide\n"
f"# Generated {datetime.now(timezone.utc).isoformat()}Z by scripts/curate_roles.py\n"
f"# Edit seed_data/role_curation.yaml then re-run; do NOT hand-edit this file.\n\n"
+ yaml.safe_dump({"roles": out_roles}, sort_keys=False, allow_unicode=True, width=200),
encoding="utf-8",
)
LOG_FILE.write_text("\n".join(log_sections), encoding="utf-8")
print(f"\nWrote {OUTPUT_FILE}")
print(f"Wrote {LOG_FILE}")
print(f"Total: {len(out_roles)} roles, {sum(len(r['skills']) for r in out_roles)} kept skills")
return 0
if __name__ == "__main__":
sys.exit(main())
|