agentsclimarketplace

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).

Install
npx -y skills add jasonChen0604/codebase-to-portfolio --skill generate-tech-profile-json

Assembled 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:

  1. Load existing tech-profile/meta.json → get id_map and source_version
  2. Load all tech-profile/projects/*.<primary_lang>.json → build in-memory cache
  3. 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>.json for each configured language
  4. Always rebuild index files (domains, tag-index, product-groups) from all project files
  5. Write meta.json with 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 the profile JSON block (if years_of_experience is absent, derive from the oldest project's last commit)
  • doc_filename (default CLAUDE.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-init first.

Step 2: Filter projects

Read projects-list.md, filter rows where:

  1. "Active" column is ✅
  2. Doc-file column is ✅
  3. "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_description
  • core_tech, database, deployment, featured
  • Also extract the Highlights bullet text from ## 📋 Portfolio Summary section 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:

  1. Extract the core system/feature name from one_line_description — translate per each configured language's file
  2. Append role suffix: -web— Frontend, -server— Backend, -nest-server— Backend (NestJS), -docker— Docker / Infra, -ios / -android— iOS App / — Android App; or derive from core_tech
  3. If one_line_description also 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] where relPath is the project folder path relative to scan_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 __N suffix before re-hashing.
  • Use display_name as the name field in projects[]. Store the original project_name as raw_name for internal reference only (omit from output JSON).
  • In domains[].skills[].projects[]: store only the id strings — no other fields. Consumer looks up full data in projects[] by id.
  • In product_groups[].projects[]: store { id, role, status } only — no display_name field.

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):

  1. Projects sharing the same parent directory path → one product group
  2. Projects whose project_name shares 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 with display_name, role (Frontend / Backend / Backend NestJS / Docker / Infra / CLI), and status

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.length exactly — this is the source of truth, not a separate estimate
  • Derive level from project_count and the statuses of the matched projects:
    • expert: project_count ≥ 5, and ≥1 is Production or In Progress
    • proficient: project_count 2–4, or project_count = 1 with Production status
    • familiar: 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 scale
  • linkedin.about: LinkedIn-optimized About section, 3–5 sentences, first-person, action verbs

Step 8: Build linkedin.skills_list

Take all tags, sort by:

  1. Tags appearing in featured: true projects first
  2. 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:

  1. tech-profile/projects/<hash>.<lang>.json for each project and each configured language (only new/changed ones on incremental runs)
  2. tech-profile/domains-<lang>.json for each configured language
  3. tech-profile/tag-index-<lang>.json for each configured language
  4. tech-profile/product-groups-<lang>.json for each configured language
  5. tech-profile/meta.json — include id_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

idLabel
frontendFrontend
backendBackend
ai_llmAI / LLM
databaseDatabase
devopsDevOps / Infrastructure
cloudCloud Services
mobileMobile
toolsTools & Automation
languagesLanguages
otherOther

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

Englishbadge
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: true projects are marked with "featured": true in 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_about field must be copy-paste ready — no placeholders, no markdown formatting, plain text only
  • tag_index must be sorted by project_count descending
  • projects array sorted by: status priority (Production first) then alphabetically by name
  • domains array ordered per the domain IDs table above (frontend → backend → ai_llm → database → devops → cloud → mobile → tools → languages → other)
  • Privacy: display_name must 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 against privacy_blocklist and re-derive from one_line_description if any token is found. This rule applies everywhere in the JSON output including projects[], domains[].skills[].projects[], product_groups[], and linkedin.experience_highlights.
  • Grouping: product_groups is additive — individual projects still appear in projects[]. product_groups gives the consumer a product-level view for portfolio/resume rendering.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.