Generate tech profile json
Skill jasonChen0604/codebase-to-portfolio/skills/generate-tech-profile-json
Turn your entire codebase history into a portfolio-ready tech profile — with any AI coding agent (Claude Code, Codex, Copilot, Cursor).
npx -y skills add jasonChen0604/codebase-to-portfolio --skill generate-tech-profile-jsonAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 29 days oldThe repository was created 29 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Generate structured JSON tech profile files from all latest-version project docs. Outputs a split-file directory tech-profile/ (not monolithic JSON) for efficient incremental updates. Triggers when the user says "generate tech profile json", "build json profile", "export json profile", "生成 JSON 技術檔案", "產出 JSON 技術樹", or "建立 JSON 技術報告".
SKILL.md
17.5 KB, as published. Nobody here has run it
Generate Tech Profile JSON Skill
Goal
Read all project docs (doc_filename) with the latest skill_version, extract structured frontmatter data, then generate a tech-profile/ split-file directory (schema version 2.0).
Output layout:
tech-profile/
├── meta.json # meta + profile + linkedin + id_map
├── domains-<lang>.json # one per configured language
├── tag-index-<lang>.json # one per configured language
├── product-groups-<lang>.json # one per configured language
└── projects/
├── <hash>.<lang>.json # one file per project, per configured language
Why split? Each file is 1–15KB. Incremental runs only rewrite changed project files + rebuild the index files. Token cost scales with changed projects, not total project count.
Incremental update logic:
- Load existing
tech-profile/meta.json→ getid_mapandsource_version - Load all
tech-profile/projects/*.<primary_lang>.json→ build in-memory cache - For each project: compute hash, compare cached
skill_version- Unchanged → skip (reuse cached file, zero token cost)
- New or changed → read the doc file, write
<hash>.<lang>.jsonfor each configured language
- Always rebuild index files (domains, tag-index, product-groups) from all project files
- Write
meta.jsonwith updated totals
On first run (no existing tech-profile/), process all projects.
Step 0: Read config
Read profile.config.json from the current working directory. If missing, tell the user to copy examples/profile.config.example.json to profile.config.json and fill it in, then stop.
profile.name,profile.email,profile.title,profile.years_of_experience— used directly in theprofileJSON block (ifyears_of_experienceis absent, derive from the oldest project's last commit)doc_filename(defaultCLAUDE.md)languages(default["en"])timezone(default+00:00)privacy_blocklist(default[])custom_domains(default{}) — merged into the built-in domain table in Step 4
JSON Schema
Each language's file set conforms to this schema:
{
"meta": {
"generated_at": "2026-06-19T12:00:00+00:00", // ISO8601, configured timezone
"source_version": "2.0",
"total_projects": 95,
"lang": "en"
},
"profile": {
"name": "<profile.name from config>",
"title": "<profile.title from config>",
"title_alt": "<an alternate phrasing of title>",
"email": "<profile.email from config>",
"summary": "<paragraph>", // 3–5 sentences, synthesized from all Highlights, in this file's language
"linkedin_about": "<paragraph>", // LinkedIn-ready About section, in this file's language
"years_of_experience": 8, // from config, or derived from oldest project last_commit
"total_projects": 95
},
"domains": [
{
"id": "frontend", // snake_case, stable identifier
"label": "Frontend", // translated per this file's language
"icon": "🖥",
"project_count": 10,
"summary": "<paragraph>", // domain expertise paragraph, in this file's language
"skills": [
{
"tag": "React",
"level": "expert", // "expert" | "proficient" | "familiar" — derived from project count + status
"project_count": 8, // MUST equal projects.length — derived from it, not estimated separately
"projects": [ // ALL project ids that have this tag — no sampling, no truncation
"project-management-system-frontend",
"lab-space-rack-management-frontend"
// ... every matching project id
]
}
]
}
],
"projects": [
{
"id": "3a7f2c1b09e4", // SHA-256[:12] of project folder path relative to scan_root
"name": "Project Management System — Frontend", // display_name (privacy-safe), never a brand/client name
"category": "Enterprise Web Application", // translated per this file's language
"status": "Production", // translated per this file's language
"status_badge": "[P]",
"featured": false,
"tags": ["React", "NestJS", "Azure AD", "WebSocket", "CASL"],
"core_tech": "TypeScript / React 18 + Vite 3",
"database": "PostgreSQL, Redis",
"deployment": "Docker Swarm / GitLab CI",
"description": "...", // translated per this file's language
"domain_primary": "frontend", // primary domain id
"domains": ["frontend", "backend"] // all applicable domain ids
}
],
"tag_index": [
{
"tag": "React",
"domain_id": "frontend",
"domain_label": "Frontend", // translated per this file's language
"project_count": 8,
"level": "expert"
}
],
"linkedin": {
"headline": "<profile.title> | key tech A · key tech B · key tech C", // translated per this file's language
"about": "<3–5 sentence paragraph>", // same as profile.linkedin_about, formatted for direct paste
"skills_list": ["TypeScript", "React", "NestJS", "Next.js", "Docker", "PostgreSQL", "LangGraph", "Azure OpenAI"],
// top 20 skills sorted by: featured projects first, then project_count desc
"experience_highlights": [
// For each domain with ≥3 projects, one bullet point summarizing key projects by display_name (no raw project codes)
"Led development of an enterprise platform serving multiple business units, built with NestJS + Next.js + Docker Swarm."
]
},
"product_groups": [
{
"product_name": "Lab Space & Rack Management",
"project_count": 4,
"roles": ["Frontend", "Backend (Laravel)", "Backend (NestJS)", "Docker / Infra"],
"status": "Production",
"projects": [
{ "id": "lab-space-rack-management-frontend", "role": "Frontend", "status": "Production" }
]
// projects[] contains id + role + status only — full data lives in projects[]
}
]
}
Execution Steps
Step 1: Get the latest version
Run the /get-latest-version skill.
- If
none, abort and tell the user to run/batch-initfirst.
Step 2: Filter projects
Read projects-list.md, filter rows where:
- "Active" column is ✅
- Doc-file column is ✅
- "Version" column equals the latest version string
Collect each matching project's Path column. Expand ~ via $HOME.
Step 3: Read project doc frontmatter (batch up to 10 in parallel)
For each project, read <path>/<doc_filename> and extract:
project_name,category,status,tags(array),one_line_descriptioncore_tech,database,deployment,featured- Also extract the
Highlightsbullet text from## 📋 Portfolio Summarysection body - Also record
path— the project's absolute path (used for grouping in Step 3b)
After extracting fields, derive a display_name for each project:
Privacy-Safe Display Name Rules
The core rule: display_name must describe only system function — never a client, company, brand, or product name.
Two categories of names require sanitization:
Category 1 — Project code prefixes (structural identifiers in project_name):
- Patterns: any 2–3 letter prefix followed by 2 digits (e.g.
pa27,pt07,wp18) - Action: strip prefix, derive name from
one_line_description
Category 2 — Client/brand/product names (appear in project_name directly or as the display name after stripping suffixes):
- Names listed in the config's
privacy_blocklist— apply judgment for any unfamiliar name that looks like a client/company/brand token too - Also applies when the derived display name itself still contains a brand/client name — re-derive from
one_line_description - Action: replace with a purely functional description from
one_line_description
Deriving a safe display name:
- Extract the core system/feature name from
one_line_description— translate per each configured language's file - Append role suffix:
-web→— Frontend,-server→— Backend,-nest-server→— Backend (NestJS),-docker→— Docker / Infra,-ios/-android→— iOS App/— Android App; or derive fromcore_tech - If
one_line_descriptionalso contains the client/brand name, paraphrase around the function not the brand
Final check before writing output: scan every display_name against privacy_blocklist. If found, re-derive.
- Assign each project a stable
id:SHA-256(relPath)[:12]whererelPathis the project folder path relative toscan_root(e.g."clients/acme/acme-lab-space-and-rack-management-web"). This keeps IDs opaque (no brand/client leak) and stable (path doesn't change unless project is moved). In case of collision (extremely rare), append__Nsuffix before re-hashing. - Use
display_nameas thenamefield inprojects[]. Store the originalproject_nameasraw_namefor internal reference only (omit from output JSON). - In
domains[].skills[].projects[]: store only theidstrings — no other fields. Consumer looks up full data inprojects[]by id. - In
product_groups[].projects[]: store{ id, role, status }only — nodisplay_namefield.
Step 3b: Group projects by product
After deriving display names, group projects belonging to the same product/system:
Grouping logic (apply both; either match triggers grouping):
- Projects sharing the same parent directory path → one product group
- Projects whose
project_nameshares a common prefix after stripping role suffixes (-web,-server,-nest-server,-docker,-backend,-frontend,-api,-cli) → one product group
For each group:
product_name: the shared functional name without role suffix (anonymized per display name rules)projects: array of members, each withdisplay_name,role(Frontend / Backend / Backend NestJS / Docker / Infra / CLI), andstatus
Add a product_groups top-level array to every language's file set (after tag_index):
"product_groups": [
{
"product_name": "Lab Space & Rack Management",
"project_count": 4,
"roles": ["Frontend", "Backend (Laravel)", "Backend (NestJS)", "Docker / Infra"],
"status": "Production", // highest-priority status among members
"projects": [
{ "id": "3a7f2c1b09e4", "role": "Frontend", "status": "Production" },
{ "id": "9f1bc234a5e7", "role": "Backend (Laravel)", "status": "Production" },
{ "id": "c82d0f6173ab", "role": "Backend (NestJS)", "status": "Production" },
{ "id": "7e45a912b0cf", "role": "Docker / Infra", "status": "Production" }
]
}
]
Individual projects still appear in the projects[] array — product_groups is additive.
Step 4: Build domain mapping
Use the domain → tags mapping from the generate-tech-profile skill (same built-in table), merged with custom_domains from config.
For each tag in any project, assign it to a domain. A tag may belong to multiple domains.
Step 5: Derive skill levels
For each (domain, tag) pair, collect every project that has this tag — no sampling, no truncation. Then:
- Set
projects= the complete array of those project ids (strings only) — ALL of them - Set
project_count=projects.lengthexactly — this is the source of truth, not a separate estimate - Derive
levelfromproject_countand the statuses of the matched projects:expert:project_count≥ 5, and ≥1 is Production or In Progressproficient:project_count2–4, orproject_count= 1 with Production statusfamiliar:project_count= 1, status is Archived / Prototype / Completed only
Step 6: Derive years_of_experience
Use profile.years_of_experience from config if set. Otherwise, not reliably derivable from project docs — output null and note in the summary that the user should set it manually.
Step 7: Build profile.summary and linkedin fields
Synthesize from all Highlights texts, once per configured language:
profile.summary: 3–5 sentence paragraph covering top domains, key technologies, and scalelinkedin.about: LinkedIn-optimized About section, 3–5 sentences, first-person, action verbs
Step 8: Build linkedin.skills_list
Take all tags, sort by:
- Tags appearing in
featured: trueprojects first - Then by project_count descending Take top 20.
Step 9: Build linkedin.experience_highlights
For each domain with ≥3 projects, write one bullet, first-person, action verb, mentioning key projects by display name, including scale/impact if available, in this file's language.
Step 10: Write split files
Write to tech-profile/ under the current working directory:
tech-profile/projects/<hash>.<lang>.jsonfor each project and each configured language (only new/changed ones on incremental runs)tech-profile/domains-<lang>.jsonfor each configured languagetech-profile/tag-index-<lang>.jsonfor each configured languagetech-profile/product-groups-<lang>.jsonfor each configured languagetech-profile/meta.json— includeid_map: { "<hash>": "<display_name in primary language>" }for all projects
meta.json structure:
{
"meta": { "generated_at", "source_version", "total_projects", "schema_version": "2.0", "lang": "<primary language>" },
"profile": { ...same as before... },
"linkedin": { ...same as before... },
"id_map": {
"3a7f2c1b09e4": "Lab Space & Rack Management — Frontend",
...
}
}
Use 2-space indentation. Ensure valid JSON (no trailing commas, no comments).
Step 11: Report results
✅ Tech profile JSON generated (split-file format)
- Projects: N total, M updated (skill_version: X.Y)
- Unique tags: T
- Domains: [frontend, backend, ai_llm, ...]
- Languages: [en, ...]
- tech-profile/ → <absolute path>
├── meta.json (id_map: N entries)
├── domains-<lang>.json (one per language)
├── tag-index-<lang>.json (one per language)
├── product-groups-<lang>.json (one per language)
└── projects/ (N × languages files)
Domain IDs and Labels
| id | Label |
|---|---|
| frontend | Frontend |
| backend | Backend |
| ai_llm | AI / LLM |
| database | Database |
| devops | DevOps / Infrastructure |
| cloud | Cloud Services |
| mobile | Mobile |
| tools | Tools & Automation |
| languages | Languages |
| other | Other |
Translate labels per configured language using the same translation approach as generate-tech-profile (Domain name translation table). Use icon: 🖥 frontend, ⚙️ backend, 🤖 ai_llm, 🗄 database, 🚀 devops, ☁️ cloud, 📱 mobile, 🔧 tools, 💻 languages, 📦 other.
Cloud domain tags
Tags that belong to cloud (remove from other/mobile/ai_llm if present):
Firebase, Firebase Admin, Firestore, Firebase FCM, Firebase Hosting,
Azure AD, Azure OpenAI, Azure Functions,
Google Cloud Storage, Google Maps, Google Sign-In, Gmail API, Google Maps Geocoding API,
Serverless, Self-Hosted
Status Translation
| English | badge |
|---|---|
| Production | [P] |
| In Progress | [IP] |
| Maintenance | [M] |
| Side Project | [S] |
| Archived | [A] |
| Prototype | [Proto] |
| Completed | [C] |
Translate status labels per configured language using the same translation table as generate-tech-profile.
Notes
- Read project docs in parallel batches of up to 10
- Write project files in parallel batches; write index files after all projects are done
- Skip malformed doc files, log warning, continue
- For each non-source language file: translate all description/summary text into natural target-language text
- For the file matching the source data's original language: descriptions may be used as-is; still translate domain/status labels
featured: trueprojects are marked with"featured": truein the JSON — do NOT add ⭐ emoji inside JSON strings- Always overwrite index files (domains, tag-index, product-groups, meta) on each run; project files are only rewritten when changed
- The
linkedin_aboutfield must be copy-paste ready — no placeholders, no markdown formatting, plain text only tag_indexmust be sorted byproject_countdescendingprojectsarray sorted by: status priority (Production first) then alphabetically by namedomainsarray ordered per the domain IDs table above (frontend → backend → ai_llm → database → devops → cloud → mobile → tools → languages → other)- Privacy:
display_namemust describe only system function — never a client, company, or brand name. This applies to ALL projects, not just those matching structural code prefixes. After deriving every display name, scan it againstprivacy_blocklistand re-derive fromone_line_descriptionif any token is found. This rule applies everywhere in the JSON output includingprojects[],domains[].skills[].projects[],product_groups[], andlinkedin.experience_highlights. - Grouping:
product_groupsis additive — individual projects still appear inprojects[].product_groupsgives the consumer a product-level view for portfolio/resume rendering.