API Manifest and Versioning

Home > Manifest and Versioning

tsujikiri can track the binding surface (API) of your C++ headers over time using a manifest — a JSON snapshot of what is currently exposed. Two manifests can be compared to detect breaking changes and suggest a semantic version bump.


What the Manifest Captures

The manifest is computed from the filtered and transformed IR, after all emit=False nodes have been removed. It records the binding-visible surface — names after any renames, types before format-level remapping:

  • Classes: their binding name, all emitted constructor signatures, all emitted methods (name, parameters, return type, is_static), all emitted fields (name, type, is_const), and nested enums

  • Free functions: name, parameter types, return type

  • Top-level enums: name and all value names with their integer values

The manifest does not capture:

  • Suppressed nodes (emit=False)

  • Template-level type remapping (from type_mappings in .output.yml)

  • Code injections

  • Comments or generation settings

Deterministic UID

The uid field is a SHA-256 hash of the api section serialised with sorted keys. The same C++ surface always produces the same uid, regardless of file ordering or timestamp.


Manifest JSON Structure

{
  "module": "myproject",
  "version": "1.2.0",
  "api": {
    "classes": [
      {
        "name": "Vec3",
        "constructors": [
          [],
          ["float", "float", "float"]
        ],
        "methods": [
          {
            "name": "length",
            "params": [],
            "return_type": "float",
            "is_static": false
          },
          {
            "name": "dot",
            "params": ["const Vec3 &"],
            "return_type": "float",
            "is_static": false
          }
        ],
        "fields": [
          { "name": "x_", "type": "float", "is_const": false },
          { "name": "y_", "type": "float", "is_const": false },
          { "name": "z_", "type": "float", "is_const": false }
        ],
        "enums": []
      }
    ],
    "functions": [
      {
        "name": "computeArea",
        "params": ["double"],
        "return_type": "double"
      }
    ],
    "enums": [
      {
        "name": "Color",
        "values": [
          { "name": "Blue", "value": 2 },
          { "name": "Green", "value": 1 },
          { "name": "Red", "value": 0 }
        ]
      }
    ]
  }
}

Tip: Commit the manifest JSON file to version control alongside the generated bindings. This gives you a complete history of API changes.


Saving and Loading a Manifest

# First run — generate bindings and save the initial manifest
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
          -m api.manifest.json

# Subsequent runs — compare with existing manifest, then save updated one
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
          -m api.manifest.json

When -m FILE is passed:

  • If FILE does not exist: generate bindings normally, then save the manifest.

  • If FILE does exist and the uid differs: compare the two manifests, print a report, then save the new manifest.

  • If FILE exists and uid is identical: no changes; keep the existing manifest unchanged.


Comparing Manifests — Breaking vs Additive

When the manifest changes, tsujikiri classifies each difference:

Breaking Changes (scripts that use the old surface may break)

What changed

Example

Class removed

Vec3 was removed

Constructor removed

Vec3() was removed

Method signature removed or changed

Vec3.length() float was removed or changed

Field removed

Vec3.x_ was removed

Field type changed

Vec3.x_: floatdouble

Field const qualifier changed

Vec3.x_ const: falsetrue

Enum removed

Color was removed

Enum value removed

Color.Red was removed

Enum value integer changed

Color.Red: 0 → 1

Additive Changes (existing scripts continue to work)

What changed

Example

Class added

Matrix4 was added

Constructor overload added

Vec3(float, float, float) was added

Method added

Vec3.normalize() Vec3 was added

Method overload added

add(double, double) double overload was added

Field added

Vec3.w_ was added

Enum added

BlendMode was added

Enum value added

Color.Alpha was added

Stderr Output

WARNING: Additive API changes:
  + Class 'Matrix4' was added
  + Method 'Vec3.normalize() -> Vec3' was added

ERROR: Breaking API changes detected:
  ! Method 'Vec3.dot(const Vec3 &) -> float' signature was removed or changed
  ! Field 'Vec3.z_' was removed

--check-compat — Fail on Breaking Changes

tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
          -m api.manifest.json --check-compat

When --check-compat is passed:

  • If breaking changes are detected: exit with code 1; the manifest is not saved (the old manifest is preserved)

  • If only additive changes: exit 0; manifest is saved normally

  • If no changes: exit 0; manifest is unchanged

Use --check-compat in CI to block merges that would break existing Lua scripts.


Semantic Versioning Integration

If the existing manifest has a "version" field that is a valid MAJOR.MINOR.PATCH semver string, tsujikiri suggests a bumped version:

Change type

Bump

Breaking changes present

Bump MAJOR, reset MINOR and PATCH to 0

Only additive changes

Bump MINOR, reset PATCH to 0

No changes

Version unchanged

INFO: Suggested semver bump: 1.2.0 -> 2.0.0

The suggestion is printed to stderr. The manifest is saved with the suggested version automatically.

To seed the initial version, manually edit the saved manifest JSON and set "version": "1.0.0". On the next run, tsujikiri will pick it up and suggest bumps from there.


--embed-version — Version Hash in Generated Code

tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
          -m api.manifest.json --embed-version

When --embed-version is passed (or embed_version: true in generation), the api version number is embedded in the generated code.

luabridge3 output:

static constexpr const char* k_myproject_api_version = "1.7.3";

const char* get_myproject_api_version()
{
    return k_myproject_api_version;
}

// Inside register_myproject():
.addFunction("get_api_version", +[] () -> const char* { return k_myproject_api_version; })

luals output:

---@return string
function myproject.get_api_version() end

Runtime version check (Lua side):

local function parse(v)
    local a, b, c = v:match("(%d+)%.(%d+)%.(%d+)")
    return a*1e6 + b*1e3 + c
end

local expected = "1.7.3"
local actual = myproject.get_api_version()
if parse(actual) ~= parse(expected) then
    error(("API version mismatch: expected %s got %s"):format(expected, actual))
end

This lets you detect at runtime when a Lua script was compiled against a different API version than the loaded library provides.


Complete CI Workflow Example

The following shell script demonstrates a full versioning workflow in a CI pipeline:

#!/bin/bash
set -euo pipefail

INPUT="project.input.yml"
MANIFEST="api.manifest.json"
OUTPUT="src/lua_bindings.cpp"

echo "--- Generating bindings ---"
tsujikiri -i "$INPUT" --target luabridge3 "$OUTPUT" -m "$MANIFEST" \
  --check-compat \
  --embed-version

# If we get here, either:
#   a) No manifest existed yet (first run), or
#   b) Changes were only additive (MINOR bump applied), or
#   c) No changes at all

echo "--- Generating LuaLS annotations ---"
tsujikiri -i "$INPUT" --target luals "types/myproject.lua"

echo "--- Committing updated bindings ---"
git add "$OUTPUT" "$MANIFEST" "types/myproject.lua"
git diff --staged --quiet || git commit -m "chore: update generated bindings"

If the C++ API has breaking changes, tsujikiri exits with code 1 at the --check-compat step, the script stops (due to set -e), and CI marks the build as failed.

Sample manifest after a breaking change is resolved and MAJOR bumped:

{
  "module": "myproject",
  "version": "2.0.0",
  "api": {
    "classes": [ ... ],
    "functions": [ ... ],
    "enums": [ ... ]
  }
}

See Also