Skip to content

feat: add deep-merge utility for profile-based metadata overrides#166

Merged
yunanwg merged 3 commits intofeat/profile-based-metadata-rfcfrom
feat/deep-merge-utility
Mar 17, 2026
Merged

feat: add deep-merge utility for profile-based metadata overrides#166
yunanwg merged 3 commits intofeat/profile-based-metadata-rfcfrom
feat/deep-merge-utility

Conversation

@yunanwg
Copy link
Owner

@yunanwg yunanwg commented Mar 17, 2026

Summary

  • Adds a deep-merge recursive dictionary merge utility (src/utils/merge.typ) exported from the package, replacing the full-copy-per-profile approach with sparse overrides
  • Restores shared root metadata.toml and modules_<lang>/ directories; profile TOML files in profiles/ only contain fields that differ
  • Updates all documentation: recipes (new "Profile-Based Overrides" section), getting-started, migration guide (rewritten for deep-merge approach), and API reference

Closes #142

Test plan

  • just build — compiles without profile (backward compat)
  • --input profile=fr — French profile overrides apply correctly
  • --input profile=fr --input language=en — language override takes precedence
  • Letter builds with and without profile
  • just docs-generate-api — API reference includes deep-merge()
  • --input profile=zh — blocked by pre-existing non-Latin font insertion bug (out of scope)

🤖 Generated with Claude Code

Replace the full-copy-per-profile approach with a deep-merge function
that recursively merges sparse profile TOML files on top of a shared
root metadata.toml. This lets users vary any field (personal.info,
layout, inject, etc.) per language or target role without duplicating
the entire configuration.

- Add src/utils/merge.typ with recursive deep-merge function
- Export deep-merge from src/lib.typ
- Update template/cv.typ and letter.typ with profile-aware loading
- Restore root metadata.toml and modules_<lang>/ directories
- Remove profile_<lang>/ directories (replaced by profiles/ overrides)
- Add example profile files (en, fr, zh) in template/profiles/
- Update docs: recipes, getting-started, migration guide, API reference

Closes #142

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spark-by-yunan spark-by-yunan bot added this to the v4.0.0 milestone Mar 17, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09d3dfb515

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

template/cv.typ Outdated
#let metadata = toml("profile_" + profile + "/metadata.toml")
#let cv-profile = sys.inputs.at("profile", default: metadata.at("profile", default: none))
#let metadata = if cv-profile != none {
deep-merge(metadata, toml("./profiles/" + cv-profile + ".toml"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve de/it profile compatibility

The new profile loader now always reads ./profiles/<profile>.toml, but this commit removed profile_de/metadata.toml and profile_it/metadata.toml without adding profiles/de.toml or profiles/it.toml. As a result, existing commands like typst compile cv.typ --input profile=de (and profile=it) now fail with a missing-file error instead of building those variants, which is a regression from the previous profile workflow.

Useful? React with 👍 / 👎.

Comment on lines +13 to +15
[personal.info.custom-2]
awesomeIcon = "car"
text = "Permis B"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear inherited link for French custom-2 entry

Because deep-merge recursively preserves unspecified nested keys, defining personal.info.custom-2 here without a link keeps the base metadata.toml link (https://aws.amazon.com/certification/). That makes the French Permis B label point to the AWS certification URL, which is incorrect output for this profile.

Useful? React with 👍 / 👎.

Restore profile_<lang>/ directory structure from PR #155 for content
modules. Slim down each profile's metadata.toml to sparse overrides
that are deep-merged with root metadata.toml. Remove the separate
profiles/ directory — overrides now live inside profile_<lang>/.

Update cv.typ and letter.typ to deep-merge root + profile metadata.
Update all docs to reflect profile_<lang>/ paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3cf6d94d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +52 to +55
#import "@preview/brilliant-cv:3.0.0": deep-merge

#let base = toml("./metadata.toml")
#let profile = toml("./profiles/fr.toml")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fix deep-merge example to use a valid import and file path

The API generator now documents deep-merge, but this snippet imports it from @preview/brilliant-cv:3.0.0 and loads ./profiles/fr.toml; both are inconsistent with this change set (which introduces deep-merge now and uses profile_<name>/metadata.toml). A repo-wide file search only finds template/profile_fr/metadata.toml and no profiles/ directory, so users copying this example will get symbol-not-found or missing-file errors.

Useful? React with 👍 / 👎.

…ance bugs

profile_fr/metadata.toml defined custom-2 with text="Permis B" but no link,
causing deep-merge to inherit root's AWS certification URL. Rename numbered
custom entries (custom-1, custom-2) to descriptive names (custom-degree,
custom-cert, custom-car) across root and all profiles. Document deep-merge
inheritance behavior and naming convention in recipes.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yunanwg yunanwg merged commit cad6d6f into feat/profile-based-metadata-rfc Mar 17, 2026
2 checks passed
@yunanwg yunanwg deleted the feat/deep-merge-utility branch March 17, 2026 12:33
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fac5333eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +34 to +36
[personal.info.custom-1]
awesomeIcon = "car"
text = "Permis B"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid inherited links in migration profile example

Step 2 tells users to keep their existing v3 metadata as the new root file, and in that layout [personal.info.custom-1] commonly already contains a link; redefining custom-1 here without link causes deep-merge to retain the old URL, so the new Permis B label can become clickable to an unrelated inherited target. Use a new key (for example custom-car) or explicitly set the link value in this example to prevent wrong output for migrated projects.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant