원클릭으로
career-clusters
Populate career cluster data files for a new industry group. Use when adding any new cluster to data/career_clusters/.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Populate career cluster data files for a new industry group. Use when adding any new cluster to data/career_clusters/.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Audit and visualize a career cluster's roles and transitions. Generates an ASCII tree diagram and level summary table, appends to CAREER_CLUSTERS_AUDIT.md, and runs referential integrity checks.
Generate real-world job title aliases for O*NET occupations and merge them into ai_resilience_scores.csv. Use when adding a new cluster or when O*NET sample titles don't reflect how jobs are actually listed.
Generate, QA, and update emerging AI-adjacent career roles for an occupation or cluster. Covers running the script, verifying job postings are real, and deciding when to regenerate vs. use cache.
| name | career-clusters |
| description | Populate career cluster data files for a new industry group. Use when adding any new cluster to data/career_clusters/. |
Populates the three cluster CSV files for a new industry group.
All three files live in data/career_clusters/:
clusters.csv — one row per cluster (append, don't overwrite)cluster_roles.csv — one row per occupation in the cluster (append). Each onet_code must appear in exactly one cluster. Cross-cluster visibility is handled via cluster_branches.csv with is_cross_family=true, not by duplicating a role in two clusters.cluster_branches.csv — one row per from→to transition (append)Schemas are in data/career_clusters/CAREER_CLUSTERS_SCHEMA.md. Read it before proceeding.
Run this to get all occupations in the target SOC group with their scores:
python3 -c "
import csv
with open('data/output/ai_resilience_scores.csv') as f:
rows = list(csv.DictReader(f))
# Filter by SOC prefix — e.g. '43' for Office/Admin, '41' for Sales, '27' for Arts/Design
prefix = '43'
matches = [(r['Code'], r['Occupation'], r['role_resilience_score'], r['final_ranking'],
r['Education'], r['Projected Growth'], r['Median Wage'])
for r in rows if r['Code'].startswith(prefix)]
matches.sort(key=lambda x: float(x[2]) if x[2] else 0)
for m in matches:
print(m)
"
SOC prefixes for the three target clusters:
434127Each cluster (clusters.csv row) is a career ladder sharing a common entry point. Grouping rules:
is_canonical=true, the rest falseFor Office/Admin (43-xxxx), expected families include:
For Sales (41-xxxx):
For Arts/Design (27-xxxx):
Append rows. Fields:
| Field | Notes |
|---|---|
cluster_id | Kebab-case slug, e.g. office-admin, retail-sales, graphic-design |
cluster_name | Display name, e.g. Office Administration |
domain | One of: Business, Sales, Creative, Technology, Healthcare, Public Safety, Transportation, Trades |
entry_onet_code | O*NET code of the true starting role (lowest Job Zone in family) |
entry_occupation | Name of that role |
entry_education | Typical entry education from enriched CSV |
entry_wage_annual | Annual median wage (parse from "Median Wage" field, extract numeric) |
notes | 1-2 sentences: what defines this family, any curation decisions worth noting |
One row per occupation. Fields:
| Field | Notes |
|---|---|
onet_code | From scores CSV |
occupation | From scores CSV |
cluster_id | Must match a row in clusters.csv |
level | 1=entry, 2=mid, 3=senior, 4=lead/advanced, 5=executive |
is_canonical | true for the primary representative at this level; false for specializations/variants |
typical_years_from_entry | 0 for entry, rough estimate for others |
notes | Why non-canonical, or anything unusual |
Canonical selection rule: when two roles are at the same level and one has significantly higher openings/median wage, it's canonical. When in doubt, use the broader title.
One row per valid transition. Fields:
| Field | Notes |
|---|---|
from_onet_code | Departing role |
to_onet_code | Destination role |
transition_type | progression / specialization / lateral |
is_primary_path | true = most common route; false = valid but secondary |
is_cross_family | true only if destination is in a different cluster |
min_years_experience_before_transition | 0 if none required |
training_cost_usd | Rough estimate; 0 for on-the-job progressions |
training_duration_years | 0 for progressions with no formal training |
can_work_during_training | true for most white-collar progressions |
notes | Source of training cost, or curation reasoning |
Specialization shortcut: specializations inherit all outbound transitions from their canonical parent. Only add explicit rows for specialization-specific exceptions.
Cross-family transitions: when a role's best next move is in a different cluster (e.g. Data Entry Clerk → Medical Records Technician, or Graphic Designer → UX Designer), add a branch row with is_cross_family=true. The destination does not need to exist in cluster_roles.csv — the career page resolves it from the main scores data. Use transition_type=lateral for peer-level pivots, progression if it's a genuine step up.
After writing all three files, run:
python3 -c "
import csv
clusters = {r['cluster_id'] for r in csv.DictReader(open('data/career_clusters/clusters.csv'))}
roles = list(csv.DictReader(open('data/career_clusters/cluster_roles.csv')))
branches = list(csv.DictReader(open('data/career_clusters/cluster_branches.csv')))
role_codes = {r['onet_code'] for r in roles}
# each onet_code must appear in exactly one cluster (one career, one cluster — transitions handled via branches)
from collections import defaultdict
code_clusters = defaultdict(list)
for r in roles:
code_clusters[r['onet_code']].append(r['cluster_id'])
dupes = {code: cls for code, cls in code_clusters.items() if len(cls) > 1}
if dupes:
print('BAD duplicate onet_codes (must be in exactly one cluster):', dupes)
# cluster_roles must reference valid cluster_ids
bad_clusters = [r for r in roles if r['cluster_id'] not in clusters]
if bad_clusters:
print('BAD cluster_id refs:', [(r['onet_code'], r['cluster_id']) for r in bad_clusters])
# branch from_onet_code must always be in cluster_roles (you own the source role)
bad_from = [b for b in branches if b['from_onet_code'] not in role_codes]
if bad_from:
print('BAD from_onet_code (must be in cluster_roles):', [b['from_onet_code'] for b in bad_from])
# branch to_onet_code only needs to be in cluster_roles if is_cross_family != true
bad_to = [b for b in branches if b['to_onet_code'] not in role_codes and b.get('is_cross_family') != 'true']
if bad_to:
print('BAD to_onet_code (not in cluster_roles and not cross-family):', [b['to_onet_code'] for b in bad_to])
cross_family = [b for b in branches if b.get('is_cross_family') == 'true']
if not dupes and not bad_clusters and not bad_from and not bad_to:
print(f'OK: {len(clusters)} clusters, {len(roles)} roles, {len(branches)} branches ({len(cross_family)} cross-family) — no ref errors')
"
Currently defined in clusters.csv:
nursing — Healthcare, entry: Nursing Assistantslaw-enforcement — Public Safety, entry: Police Officerstransit-police — Public Safety, entry: Transit Policeaviation-operations — Transportation, entry: Aircraft Service AttendantsUse these as style reference when writing new rows.
adjacent_roles.py uses three matching methods in priority order:
cluster_roles.csv + cluster_branches.csv. Branch notes are injected into the Claude prompt as ground truth for "How to make the move" steps.onet_economic_index_task_table.csvWithout a cluster entry, Method 1 produces zero candidates and adjacent roles rely entirely on task overlap and SOC similarity — lower quality, generic steps. Always populate cluster files before running adjacent_roles.py.
Once cluster files are written, Track B continues:
python3 scripts/generate_emerging_roles.py --cluster <cluster_id>
python3 scripts/generate_emerging_job_titles.py
python3 scripts/generate_next_steps.py --cluster <cluster_id>
python3 scripts/adjacent_roles.py --cluster <cluster_id>