Individualization
Overview
Divekit individualizes repositories with three different files:
variation.json: defines available object, relation, and logic variants.individualization.json(optional): defines replacement behavior and processing rules.individuals.json(generated): stores the concrete selection per UUID.
Important: per-member or per-group selection is not configured via individualization.json keys like logicId, objectAssignments, or globalSettings. Those keys are outdated.
Configuration Files
variation.json (input)
Defines what can vary:
objectsrelationslogic
Example:
{
"objects": [
{
"ids": "Vehicle",
"objectVariations": [
{ "id": "car", "type": "class", "name": "Car", "variables": { "Vehicle": "Car" } },
{ "id": "truck", "type": "class", "name": "Truck", "variables": { "Vehicle": "Truck" } }
]
}
],
"relations": [
{
"id": "ownership",
"type": "association",
"source": "Vehicle",
"target": "Owner",
"options": ["one-to-one", "one-to-many"],
"variables": {}
}
],
"logic": [
{
"id": "algorithm",
"type": "implementation",
"location": "src/logic",
"options": ["simple", "complex"],
"variables": {}
}
]
}
individualization.json (optional input)
Defines processing behavior. Current top-level keys are:
variables(required when file exists)solutionDeletionwarningsfileManipulationprocessorsrelationsvariableExtensionsobjectVariations/relationVariations/logicVariations(legacy)variations(unified container, advanced compatibility)
Minimal valid example:
{
"variables": {
"variableDelimiter": "$"
}
}
Example with common options:
{
"variables": {
"variableDelimiter": "$",
"values": {
"semester": "SS2026"
}
},
"solutionDeletion": {
"enabled": true,
"files": ["src/main/java/Solution.java"]
},
"warnings": {
"enabled": true,
"ignore": ["*.tmp"],
"variationResidues": {
"typeWhiteList": ["java", "md"],
"ignoreList": ["id", "name", "Feedback", "coffee"],
"matchMode": "word",
"ignoreRules": [
{
"paths": ["src/test/java/**"],
"values": ["Dish"]
},
{
"paths": ["src/generated/**"]
}
]
}
},
"processors": {
"uml": {
"mode": "both",
"imageFormat": "jpg"
},
"tableHash": {
"solutionTableFileName": "${id}.solution.md",
"hashedSolutionTableFileName": "${id}.hashed.md",
"tableConfigFileName": "${id}.config.json"
}
}
}
variationResidues replaces the older variableValueWarnings key.
variableValueWarnings is deprecated and kept only for backwards
compatibility. Existing repositories can still keep it, but new config should
use variationResidues.
ignoreList is global. Besides residue values such as Dish, it can also
contain the surrounding word, identifier, or path fragment that should
suppress a match, for example Feedback or coffee for a Fee residue hit.
If you set typeWhiteList, treat it as the full list of file types that should
be scanned. Include every extension you still want Divekit to inspect.
If an ignoreRules entry omits values, Divekit suppresses all matching
residue warnings and related divekit dev false positives for the matching
paths.
Target-Specific File Markers
fileManipulation also controls how files or directories are included only for specific targets.
With the built-in default settings
filePreIdentifier: "_"filePostIdentifier: "repo"noRepoIdentifier: "no"
you usually do not need to add a fileManipulation block unless you want to
override those defaults.
you can use markers such as:
_workrepo: include only in theworkrepository_evalrepo: include only in theevalrepository_norepo: exclude from all generated repositories
Examples:
src/tests_evalrepo/HiddenTests.java -> only in eval, final path: src/tests/HiddenTests.java
docs/teacher_workrepo.md -> only in work, final path: docs/teacher.md
assets/_norepo/logo.svg -> not copied to any generated repository
Legacy _mainrepo, _coderepo, and _testrepo markers are still accepted and map to work / eval as before.
Hashed Solution Tables
Divekit can generate hashed expected-answer tables for exercises where students fill Markdown table cells. The common pattern is:
src/main/resources/E1.md -> student-facing table in work
src/test/resources/E1.solution_evalrepo.md -> raw expected table in eval
src/test/resources/E1.config.json -> table hashing rules in both targets
After divekit distribute, the repositories contain:
work:
src/main/resources/E1.md
src/test/resources/E1.config.json
src/test/resources/E1.hashed.md
eval:
src/main/resources/E1.md
src/test/resources/E1.config.json
src/test/resources/E1.solution.md
_evalrepo or legacy _testrepo keeps the raw solution table out of the work
repository. Divekit reads the eval solution table and its config after normal
individualization, then writes the hashed table into the matching work
repository. The same processing also runs when existing repositories are
updated with divekit patch.
The default naming convention uses dot suffixes and usually needs no
individualization.json entry:
${id}.solution.md
${id}.hashed.md
${id}.config.json
Use processors.tableHash only when your project needs different file names,
for example to keep an older prepare-style convention:
{
"processors": {
"tableHash": {
"solutionTableFileName": "E${id}-solution_testrepo.md",
"hashedSolutionTableFileName": "E${id}-hashed-solution.md",
"tableConfigFileName": "E${id}-config.json"
}
}
}
Each table config controls which columns are hashed:
{
"tableType": "ROWS_AND_COLUMNS",
"explanationDimensions": ["Question"],
"validRowValues": [],
"validColumnValues": ["Question", "Answer"],
"validCellValues": [],
"hashedColumns": [false, true],
"showRowHints": true,
"showColumnHints": true
}
Cells are trimmed before hashing by default, so | 201 |, | 201 |, and
| 201| match the same expected value. Hashed columns are case-insensitive
by default, matching the legacy Java test library. Add caseSensitiveColumns
and set a column to true when the exact case must be preserved before hashing,
for example for URLs. Comma-separated cell values are trimmed, empty values are
ignored, and the remaining values are sorted case-insensitively before hashing.
For single inline answers, the hash directive can be used in ordinary files:
| Status code | $hash("201")$ |
The directive emits a blank value in student-facing content, writes the hash to
the work target, and writes the raw value to the eval solution artifact.
Whitespace inside the directive is trimmed before hashing, so
$hash(" 201 ")$ is treated like $hash("201")$.
UML Artifacts
Divekit treats .uxf and .uml files as UML sources during individualization.
By default, each individualized UML file produces:
- the individualized source file itself
- one rendered image with the configured image format
The default processor configuration is:
{
"processors": {
"uml": {
"mode": "both",
"imageFormat": "jpg"
}
}
}
Examples with the default settings:
docs/test1_evalrepo.uxf -> eval only: docs/test1.uxf + docs/test1.jpg
docs/test2_workrepo.uxf -> work only: docs/test2.uxf + docs/test2.jpg
docs/test3.uxf -> both targets: docs/test3.uxf + docs/test3.jpg
fileManipulation markers are resolved first. The cleaned target path is then
used for both generated artifacts.
Available modes:
off: keep only the individualized source file, without UML-specific artifact generationsource: keep only the individualized source fileimage: emit only the rendered image artifactboth: emit both source and image artifacts
If mode is omitted, Divekit uses both. If imageFormat is omitted, Divekit
uses jpg.
image and both require a working UMLet installation for rendering. source
and off do not.
individuals.json (generated output)
divekit distribute creates or updates concrete selections per UUID:
{
"version": "2.0",
"individuals": [
{
"uuid": "team-01",
"selection": {
"objects": { "Vehicle": "car" },
"relations": { "ownership": "one-to-many" },
"logic": { "algorithm": "simple" }
}
}
]
}
File Locations and Fallbacks
individualization.json is resolved in this order:
.divekit/distributions/<distribution>/individualization.json.divekit/individualization.json~/.divekit/individualization.json- built-in defaults
Variation definitions may be stored in unified individualization.json or in
legacy variation.json. Divekit resolves the variation section in this order:
.divekit/distributions/<distribution>/individualization.json.divekit/distributions/<distribution>/variation.json.divekit/individualization.json.divekit/variation.json~/.divekit/individualization.json- built-in defaults with no variations
Variations are optional. If a unified file omits variations, Divekit checks
the next source. An explicit empty "variations": {} stops that fallback and
disables variations. Use divekit inject --scope variation when you want an
example configuration; Divekit no longer installs or reads a global
~/.divekit/templates/variation.json runtime fallback.
Lua and Scripting Clarification
Lua support exists, but not as dedicated keys in individualization.json.
individualization.jsoncontrols replacement, deletion, warnings, and file handling.- Lua hooks and language packs are configured through distribution
config.json(scripting) and script files. - Use
divekit inject --scope scriptsto scaffold default scripts.
Built-In Token Transformers
The transformer set is still evolving. Behavior, naming, and aliases may still be adjusted.
The following transformer names are the most likely to stay stable:
Upper, Lower, Pascal, Camel, Kebab, Snake.
Names such as Class, Parameter, FIRST_LOWER, and FIRST_UPPER are useful today, but their exact naming and semantics are still more likely to be refined.
In particular, FIRST_LOWER / FIRST_UPPER are not the same as Lower / Upper: they only change the first character, while Lower / Upper affect the whole sanitized identifier.
For each selected object mapping, Divekit generates a base token and a set of transformed variants.
Illustrative selection result:
{
"objects": {
"Person": "Customer"
}
}
This gives you tokens such as:
$Person$-> base display value$person$-> lowercase shortcut of the base display value$PersonClass$-> class-style variant$personClass$-> lowercase shortcut for the same variant
Lowercase shortcut
For object tokens, Divekit also creates a shortcut with the first letter of the object name lowercased:
$Person$and$person$$PersonClass$and$personClass$$VehicleParameter$and$vehicleParameter$
The lowercase shortcut is useful for variable-like placeholders in prose, filenames, and code.
For generated object tokens, the transformer runs first and then the first character of the resolved value is aligned with the token name:
- tokens starting with an uppercase object name (for example
$FruitClass$) resolve to a value with an uppercase first character - tokens starting with a lowercase object name (for example
$fruitClass$) resolve to a value with a lowercase first character
Examples with the current built-ins:
$FruitClass$->Banana$fruitClass$->banana$FruitParameter$->Banana$fruitParameter$->banana
Display token
The base token without a suffix is the display-oriented form:
$Plz$$plz$
If the selected variation ID is ZipCode, the current built-in display behavior is:
$Plz$->Zip code$plz$->zip code
The display transformer is intentionally heuristic. For exact wording, prefer explicit overrides in variation.json.
Use an explicit "" value when the selected variation id is an external identifier that must stay exact.
UUID-like IDs are preserved, but other opaque IDs such as asd-012, slugs, or YouTube-style IDs may be changed by the display fallback. For those cases, define the base token explicitly:
{
"id": "asd-012",
"": "asd-012"
}
Common built-ins
The following built-in suffixes are currently available:
| Token | Typical use | Example for ShoppingBasket |
|---|---|---|
Class / CLASS | Type/class-like name | ShoppingBasket |
Parameter / PARAMETER | Parameter or variable name | shoppingBasket |
Upper / UPPER | Uppercase identifier | SHOPPINGBASKET |
Lower / LOWER | Lowercase identifier | shoppingbasket |
FIRST_UPPER | Only uppercase the first character of a sanitized identifier; does not uppercase the whole value | Shoppingbasket |
FIRST_LOWER | Only lowercase the first character of a sanitized identifier; does not lowercase the whole value | shoppingBasket |
Pascal / PASCAL | PascalCase | ShoppingBasket |
Camel / CAMEL | camelCase | shoppingBasket |
Kebab / KEBAB | kebab-case | shopping-basket |
Snake / SNAKE | snake_case | shopping_basket |
Other built-ins such as Package, Plural, Repository, Dto, Rest, RestId, and IndefArticle also exist, but they are more domain-specific and may still be adjusted.
Overriding Generated Values in variation.json
You can override the generated display value and specific transformer outputs directly inside a variation entry.
Example:
{
"version": "2.0",
"variations": {
"objects": [
{
"id": "Plz",
"kinds": [
{
"id": "ZipCode",
"": "Zip code",
"Class": "ZipCode",
"Parameter": "zipCode"
}
]
}
]
}
}
With that configuration:
"": "Zip code"overrides$Plz$and$plz$"Class": "ZipCode"overrides$PlzClass$and$plzClass$"Parameter": "zipCode"overrides$PlzParameter$and$plzParameter$
This is usually the best way to handle exact product names, acronyms, or wording that should not depend on heuristics.
The same mechanism also works for other custom field names:
{
"id": "ZipCode",
"": "Zip code",
"DocLabel": "ZIP code",
"RestLabel": "zip-code"
}
That produces tokens like:
$PlzDocLabel$$plzDocLabel$$PlzRestLabel$$plzRestLabel$
Extending Transformers with Lua
You can register additional named transformers from Lua scripts. This is useful when the built-ins are not enough or when you need language-specific formatting.
Use divekit inject --scope scripts to scaffold default Lua scripts.
Minimal example:
return function(dk)
dk.transformers.register({
token = "Slug",
transform = function(s)
local out = s:gsub("([a-z0-9])([A-Z])", "%1-%2")
out = out:gsub("[_%s]+", "-")
return out:lower()
end
})
end
This creates additional tokens such as:
$PersonSlug$$personSlug$
Notes:
- Lua transformers must have a named token such as
Slug. The built-in display token without a suffix is not currently extensible through Lua registration. - If a Lua transformer registers the same token name as an existing one, the later registration wins.
- Keep built-in names for standard semantics and use custom names for project-specific rules.
Recommended Workflow
- Run
divekit initto create distribution config. - Add or scaffold variation data (
divekit inject --scope variation), then editvariation.json. - Add
individualization.jsononly for advanced behavior (delimiter, deletion, warnings, file handling). - Validate with
divekit doctor --verbose. - Run distribution and inspect
individuals.jsonfor concrete selections.
Related pages: