Individualization

Learn how Divekit uses variation.json, individualization.json, and individuals.json for assignment 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:

  • objects
  • relations
  • logic

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)
  • solutionDeletion
  • warnings
  • fileManipulation
  • processors
  • relations
  • variableExtensions
  • objectVariations / 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 the work repository
  • _evalrepo: include only in the eval repository
  • _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 generation
  • source: keep only the individualized source file
  • image: emit only the rendered image artifact
  • both: 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:

  1. .divekit/distributions/<distribution>/individualization.json
  2. .divekit/individualization.json
  3. ~/.divekit/individualization.json
  4. 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:

  1. .divekit/distributions/<distribution>/individualization.json
  2. .divekit/distributions/<distribution>/variation.json
  3. .divekit/individualization.json
  4. .divekit/variation.json
  5. ~/.divekit/individualization.json
  6. 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.json controls 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 scripts to 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:

TokenTypical useExample for ShoppingBasket
Class / CLASSType/class-like nameShoppingBasket
Parameter / PARAMETERParameter or variable nameshoppingBasket
Upper / UPPERUppercase identifierSHOPPINGBASKET
Lower / LOWERLowercase identifiershoppingbasket
FIRST_UPPEROnly uppercase the first character of a sanitized identifier; does not uppercase the whole valueShoppingbasket
FIRST_LOWEROnly lowercase the first character of a sanitized identifier; does not lowercase the whole valueshoppingBasket
Pascal / PASCALPascalCaseShoppingBasket
Camel / CAMELcamelCaseshoppingBasket
Kebab / KEBABkebab-caseshopping-basket
Snake / SNAKEsnake_caseshopping_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.
  1. Run divekit init to create distribution config.
  2. Add or scaffold variation data (divekit inject --scope variation), then edit variation.json.
  3. Add individualization.json only for advanced behavior (delimiter, deletion, warnings, file handling).
  4. Validate with divekit doctor --verbose.
  5. Run distribution and inspect individuals.json for concrete selections.

Related pages: