Register and stage models

DearDiary.Model and DearDiary.ModelVersion form a project-scoped registry on top of the run-tracking entities. A Model is the named entry that downstream serving code refers to (e.g. "fraud-classifier"); a ModelVersion is a concrete checkpoint with lineage back to the Iteration that produced it, an optional pointer at the artifact bytes in any configured storage backend, and a lifecycle DearDiary.Stage.

Versions transition through NO_STAGE → STAGING → PRODUCTION → ARCHIVED. Promoting a version to PRODUCTION automatically demotes whichever sibling was previously in PRODUCTION, preserving the "at most one production version per model" invariant.

Scaffold a project and an iteration

julia> project_id, _ = create_project("Fraud detection");
julia> experiment_id, _ = create_experiment(project_id, DearDiary.IN_PROGRESS, "DT sweep");
julia> iteration_id, _ = create_iteration(experiment_id);
julia> create_parameter(iteration_id, "max_depth", 7);
julia> create_metric(iteration_id, "accuracy", 0.96);

Save the trained model bytes as a Resource. Any serialisation format works; the registry stores only the byte payload and its lineage.

julia> checkpoint_bytes = rand(UInt8, 1024);
julia> resource_id, _ = create_resource(experiment_id, "fraud-clf.jlso", checkpoint_bytes);

Register the model

A Model is a named entry: a stable identifier that persists across successive training runs and model versions.

julia> model_id, _ = create_model(project_id, "fraud-classifier");
julia> get_model(model_id)DearDiary.Model
 ├ id = "084adfb2-cd65-4fe1-a801-ed0145bd96e8"
 ├ project_id = "a58cc530-bfc0-4a19-a9b0-b9ef8977cd90"
 ├ name = "fraud-classifier"
 ├ description = ""
 ├ created_date = 2026-10-01T23:04:07.389
 └ updated_date = nothing

Register a version

A ModelVersion ties a Resource to the Iteration that produced it. The per-model version number is assigned on insert as one greater than the model's current highest version, and a uniqueness constraint keeps it distinct within the model:

julia> version_a_id, _ = create_modelversion(
           model_id, iteration_id, resource_id,
           "Decision tree, max_depth=7",
       );
julia> version_a = get_modelversion(version_a_id)DearDiary.ModelVersion
 ├ id = "f9cb597a-8fc4-4ea4-8503-36973dff0266"
 ├ model_id = "084adfb2-cd65-4fe1-a801-ed0145bd96e8"
 ├ version = 1
 ├ iteration_id = "763c2a83-550e-42e5-bf3a-9bad847fad45"
 ├ resource_id = "369181fb-6b6e-4e2b-a398-6f2695de6282"
 ├ stage_id = 1
 ├ description = "Decision tree, max_depth=7"
 ├ created_date = 2026-10-01T23:04:07.517
 └ updated_date = nothing

A freshly registered version starts in DearDiary.NO_STAGE. Promote it through the lifecycle as evaluation results become available:

julia> update_modelversion(version_a_id, DearDiary.STAGING, nothing, nothing);
julia> update_modelversion(version_a_id, DearDiary.PRODUCTION, nothing, nothing);

Roll forward to a new checkpoint

Train another iteration, register a second version, and promote it to PRODUCTION. The previous production version is archived by the same update_modelversion call:

julia> iteration_b_id, _ = create_iteration(experiment_id);
julia> create_parameter(iteration_b_id, "max_depth", 9);
julia> create_metric(iteration_b_id, "accuracy", 0.974);
julia> resource_b_id, _ = create_resource(experiment_id, "fraud-clf-v2.jlso", rand(UInt8, 1024));
julia> version_b_id, _ = create_modelversion( model_id, iteration_b_id, resource_b_id, "Decision tree, max_depth=9", );
julia> update_modelversion(version_b_id, DearDiary.PRODUCTION, nothing, nothing);

The previous production version is now archived:

julia> get_modelversion(version_a_id).stage_id == (DearDiary.ARCHIVED |> Integer)true
julia> get_modelversion(version_b_id).stage_id == (DearDiary.PRODUCTION |> Integer)true

Browsing the registry

get_modelversions returns the per-model history ordered by version ascending, so finding the current production checkpoint is a single filter:

julia> versions = get_modelversions(model_id);
julia> production = filter(v -> v.stage_id == (DearDiary.PRODUCTION |> Integer), versions);
julia> production[1].version2

The full lineage is reachable from version.iteration_id and version.resource_id:

julia> producing_iteration = (version_b_id |> get_modelversion).iteration_id |> get_iterationDearDiary.Iteration
 ├ id = "7840e32d-cd2e-49be-ab04-30876ebfb0a9"
 ├ experiment_id = "720e6f3b-ddf5-43a4-b8ef-1dd30b484f18"
 ├ notes = ""
 ├ created_date = 2026-10-01T23:04:07.807
 ├ end_date = nothing
 ├ parent_iteration_id = nothing
 ├ status_id = 1
 ├ error_message = ""
 ├ julia_version = ""
 ├ git_sha = ""
 ├ git_dirty = false
 ├ entrypoint = ""
 ├ project_toml = ""
 └ manifest_toml = ""
julia> get_parameters(producing_iteration.id)1-element Vector{DearDiary.Parameter}:
DearDiary.Parameter
 ├ id = "389375e7-6e4c-4d3c-bd4b-727936006611"
 ├ iteration_id = "7840e32d-cd2e-49be-ab04-30876ebfb0a9"
 ├ key = "max_depth"
 └ value = "9"

Rename a model

The registry name can change after versions exist. update_model leaves a field alone when it is nothing and returns DearDiary.Duplicate when another model in the project already uses the new name:

julia> update_model(model_id, "fraud-classifier-dt", nothing)DearDiary.Updated
julia> get_model(model_id).name"fraud-classifier-dt"

Delete rules

An iteration or artifact that a version points at cannot be deleted while that version exists, so the registry never references a missing run:

julia> delete_iteration(iteration_b_id)false

Delete the version first, then the run. Deleting a model removes its versions and keeps their artifacts. See Deleting records.

julia> delete_modelversion(version_b_id);
julia> delete_iteration(iteration_b_id)true