Skip to content

Chapter 1: Project and Persistent Record Boundary

Imagine you are studying radiation transport with RAMAL-EBX.

You create a PHITS design, generate several cases, run simulations, inspect results, and compare studies. Later, you close the application and reopen it. Everything should still belong to the correct research workspace.

That is the purpose of the Project and Persistent Record Boundary.

A Project is RAMAL-EBX’s durable workspace. It is like a laboratory filing cabinet:

  • The Project is the cabinet.
  • Designs are folders containing experiment setups.
  • Cases are prepared experiment instances.
  • Jobs and Results record execution.
  • Studies and research records store analysis and evidence.
  • Project IDs connect all of these records safely.

The boundary answers an important question:

“Does this record really belong to this Project, and can it be loaded safely?”

What problem does this solve?

Without a clear boundary, files can become mixed together:

Project A/
Project B/
shared-results/
old-case/

A result from Project A might accidentally be opened while Project B is active. A path such as ../../private-data might escape the Project directory. A damaged or replaced file might be accepted as valid.

RAMAL-EBX prevents these problems by giving each Project:

  1. A descriptor file.
  2. A stable project ID.
  3. A canonical workspace directory.
  4. Safe path rules.
  5. Validated persistent records.
  6. Atomic file updates.

The Project descriptor

A Project begins with a descriptor file ending in .ramal-project.

For example:

projects/alpha-study/
└── alpha-study.ramal-project

The descriptor contains the Project’s identity and solver configuration:

{
  "project_id": "alpha-study",
  "display_name": "Alpha Study",
  "phits_launcher": "/Users/faiz/phits/bin/phits.sh",
  "multiplier_file": "/Users/faiz/phits/data/multiplier/m200.inp"
}

The project_id is the machine-friendly identity. The display_name is the name shown to a person.

For example:

project_id:   alpha-study
display_name: Alpha Study

The ID should remain predictable and safe for filenames. The display name can be more readable.

A Project is more than one descriptor file. It owns the records created inside its workspace.

A simplified workspace might look like this:

alpha-study/
├── alpha-study.ramal-project
├── designs/
├── cases/
├── studies/
└── research/

Each directory has a role:

  • designs/ stores authored PHITS designs.
  • cases/ stores immutable simulation inputs.
  • studies/ stores grouped research runs.
  • research/ stores evidence and analysis records.

The exact files inside these folders are handled by other parts of the application. The important rule is ownership:

Records stored inside a Project workspace are expected to belong to that Project.

This boundary connects directly to the immutable lifecycle described in Immutable Case, Job, Attempt, and Result Lifecycle.

Application state versus Project data

RAMAL-EBX also has temporary application state.

For example, the current camera position is useful while the application is running, but it is not scientific Project data. The same is true for editor panel visibility or debug overlays.

The application keeps both kinds of state:

App_State :: struct {
    editor: Editor_State,
    camera: Camera_State,
    project: Project,
    design_catalog: Design_Catalog,
    case_catalog: Case_Catalog,
}

project, design_catalog, and case_catalog describe the active research workspace.

editor and camera describe the current user interface session.

This distinction prevents accidental mixing of preferences with scientific records.

Stable identity: Project IDs

Every Project needs an ID that can safely identify it.

RAMAL-EBX validates IDs with rules such as:

  • At least 3 characters.
  • No more than 64 characters.
  • Lowercase letters, numbers, and hyphens only.
  • No leading or trailing hyphen.
  • No repeated hyphens.

Valid examples:

alpha-study
shielding-2026
dose-model-a

Invalid examples:

Alpha Study
--temporary
study_

The validation rule is implemented by record_id_valid:

record_id_valid :: proc(value: string) -> bool {
    if len(value) < 3 || len(value) > 64 {return false}
    if value[0] == '-' || value[len(value)-1] == '-' {return false}
    // Remaining characters are checked here.
}

The function returns true for a safe ID and false for an invalid one.

A stable ID is important because display names may change. “Alpha Study” might later become “Alpha Shielding Study,” but the identity used by records should be managed deliberately.

Persistent records carry provenance

A persistent record should explain where it came from.

A record may contain fields like:

project_id: created-by-project
record_id:  case-001
created_at: 2026-09-11T08:30:00Z
source_id:  design-001

These fields provide provenance:

  • project_id says which Project owns the record.
  • record_id identifies the record itself.
  • created_at says when it was created.
  • source_id identifies the input or parent record.

This is similar to putting a label on every laboratory sample. Even if the sample is moved, the label explains its origin.

These relationships become especially useful in Research Provenance and Evidence Pipeline.

Safe relative paths

A Project should be able to refer to files inside its workspace without allowing paths to escape.

For example:

designs/design-001.json
cases/case-001/case-001.json
research/evidence-001.json

These are relative paths. RAMAL-EBX resolves them against the Project directory.

A dangerous path would be:

../../secret-data.json

That path attempts to move upward out of the Project workspace.

The storage layer rejects paths that are absolute, contain traversal components, or use unsafe separators:

project_relative_path_valid :: proc(value: string) -> bool {
    if len(value) == 0 || filepath.is_abs(value) {return false}
    if strings.contains(value, "..") {return false}
    // Additional separator and control-character checks follow.
}

The actual implementation performs stricter component-by-component validation. The goal is simple:

A Project-relative path must stay inside the Project workspace.

A path can look like it belongs to a Project while secretly pointing somewhere else through a symbolic link.

For example:

alpha-study/cases -> /another/location

RAMAL-EBX checks directory information using lstat. This allows it to notice symbolic links instead of silently following them.

project_directory_is_canonical :: proc(path: string) -> bool {
    info, stat_error := os.lstat(path, context.temp_allocator)
    canonical, canonical_error := filepath.abs(path, context.temp_allocator)
    return stat_error == nil &&
        info.type == .Directory &&
        canonical_error == nil &&
        canonical == path
}

This means the directory must:

  1. Exist.
  2. Really be a directory.
  3. Have the expected absolute path.
  4. Not be an unexpected symbolic link.

This protects the Project boundary from accidental or malicious redirection.

Solving the central use case

Suppose the user wants to open:

projects/alpha-study/alpha-study.ramal-project

RAMAL-EBX must load the Project and all of its owned catalogs.

The high-level flow is:

  1. Check that the descriptor path is valid.
  2. Read and parse the descriptor.
  3. Validate its keys and values.
  4. Load Designs, Cases, Studies, and Research records.
  5. Replace the active workspace only if everything succeeds.

The important safety rule is:

Do not partially replace the current Project with half-loaded data.

A simplified version of the loading idea looks like this:

project: Project
if !project_load(descriptor_path, &project) {
    return false
}
if !design_catalog_load(&project, &design_catalog) {
    return false
}

If either step fails, the Project is not accepted as ready.

Loading the application

At startup, app_init_state reads the descriptor path from the editor state:

project_descriptor := editor_buffer_text(
    app.editor.project_descriptor_path[:],
)
if len(project_descriptor) > 0 {
    _ = project_load(project_descriptor, &app.project)
}

If the Project loads successfully, RAMAL-EBX loads its related catalogs:

if app.project.loaded &&
   !design_catalog_load(&app.project, &app.design_catalog) {
    log.error("The active project has an invalid design catalog")
    return false
}

The same pattern is used for Cases, Studies, and Research records.

This gives the application a reliable startup sequence:

flowchart TD
    A[Read descriptor path] --> B[Load Project descriptor]
    B --> C[Validate Project identity]
    C --> D[Load Designs, Cases, Studies, Research]
    D --> E[Active Project is ready]

Opening a different Project safely

When the user opens another Project, RAMAL-EBX first loads everything into temporary staging containers.

staged_project := Project{}
staged_design_catalog := Design_Catalog{}
staged_cases := Case_Catalog{}
staged_study_catalog := Study_Catalog{}
staged_index := Research_Index{}

The staged data is checked before it becomes active:

if !project_load(path, &staged_project) ||
   !design_catalog_load(&staged_project, &staged_design_catalog) {
    return false
}

Only after all required data loads successfully does the application replace the current state:

app.project = staged_project
app.design_catalog = staged_design_catalog
app.case_catalog = staged_cases

This is similar to preparing a new laboratory cabinet in a separate room. You inspect every folder first. Only then do you move it into the main laboratory.

The complete relationship is:

sequenceDiagram
    participant User
    participant App as Application
    participant Stage as Staging State
    participant Disk

    User->>App: Open Project
    App->>Disk: Read descriptor and records
    Disk-->>Stage: Return candidate data
    Stage->>Stage: Validate ownership and structure
    Stage-->>App: Accept complete Project
    App-->>User: Show new workspace

If validation fails, the active Project remains untouched.

Creating a Project

Creating a Project follows the reverse process.

The application first validates:

  • The descriptor filename.
  • The parent directory.
  • The new Project ID.
  • The display name.
  • The solver paths.

Then it creates the workspace and writes the descriptor.

document, valid := project_document_new(
    project_id,
    display_name,
    phits_launcher,
    multiplier_file,
)
if !valid {
    return false
}

The descriptor is written exclusively:

return storage_write_exclusive(path, data)

“Exclusive” means creation fails if a file already exists. This helps prevent accidentally replacing another Project descriptor.

Atomic replacement

Sometimes an existing descriptor must be updated, such as when changing the display name or PHITS path.

RAMAL-EBX writes the new content to a temporary file first:

temporary := strings.concatenate(
    {path, ".ramal-tmp"},
    context.temp_allocator,
)
if !storage_write_exclusive(temporary, data) {
    return false
}

Then it renames the temporary file into place:

if os.rename(temporary, path) != nil {
    _ = os.remove(temporary)
    return false
}
return true

This is called atomic replacement.

Conceptually:

flowchart LR
    A[Existing descriptor] --> B[Write temporary descriptor]
    B --> C[Validate write]
    C --> D[Rename temporary file]
    D --> E[New descriptor becomes visible]

The user should not observe a half-written JSON document. Either the old version remains, or the new version appears.

Detecting stale or tampered data

The Project boundary also helps identify suspicious data.

Examples include:

  • A descriptor has unknown JSON keys.
  • A Project ID is invalid.
  • A record points to a different Project ID.
  • A required file is a directory or symlink.
  • A path escapes the workspace.
  • A record is missing its source relationship.
  • A timestamp is malformed.

The descriptor schema is intentionally strict:

project_document_keys_valid :: proc(data: []byte) -> bool {
    value, parse_error := json.parse(data)
    object, object_ok := value.(json.Object)
    if parse_error != nil || !object_ok || len(object) != 4 {
        return false
    }
    // Only the four known keys are accepted.
}

Strict schemas make errors visible instead of silently guessing what damaged data might mean.

Clearing a Project

When a Project is closed, its owned data must be released from the active session.

project_clear(&app.project)
design_catalog_clear(&app.design_catalog)
case_catalog_clear(&app.case_catalog)

app_project_close also clears active scenes, simulation state, studies, research indexes, and temporary caches.

However, it preserves interface preferences such as:

active_panel := app.editor.active_panel
show_debug_overlays := app.editor.show_debug_overlays

This is another example of the boundary:

  • Project-owned research data is cleared.
  • Application viewing preferences remain available.

Project paths in practice

To resolve a file inside the active Project, code can use a relative path:

path, ok := project_resolve_path(
    &app.project,
    "designs/design-001.json",
)
if !ok {
    return false
}

If the Project workspace is:

projects/alpha-study

the resolved path becomes:

projects/alpha-study/designs/design-001.json

A path outside the workspace is rejected:

path, ok := project_resolve_path(
    &app.project,
    "../../secret.json",
)

Here, ok is false, and no unsafe path is returned.

Project catalogs

RAMAL-EBX maintains a Project catalog so users can find known Projects.

The catalog can scan the projects/ directory:

project_catalog_refresh(
    &app.project_catalog,
    app.project.descriptor_path,
)

It can also remember external Project descriptors. External paths are stored in a small registry, but paths inside the normal projects/ directory do not need to be duplicated there.

This gives users a convenient list while keeping the Project itself as the source of truth.

How the pieces fit together

The main ideas can be summarized like this:

flowchart TD
    P[Project descriptor] --> I[Project identity]
    P --> S[Solver configuration]
    P --> W[Workspace directory]
    W --> D[Design records]
    W --> C[Case records]
    W --> J[Jobs and Results]
    W --> R[Research records]
    W --> G[Safe path and storage rules]

The descriptor identifies the workspace. The workspace contains owned records. Storage rules ensure that records stay in the correct place and are written safely.

Where this code lives

The main Project model is in project.odin.

It defines:

  • Project_Document
  • Project
  • Project creation
  • Project loading
  • Project duplication
  • Project renaming
  • Project deletion
  • Project catalog management

Path and record safety helpers are in project_storage.odin.

They handle:

  • Record ID validation
  • Relative path validation
  • Workspace containment
  • Symlink checks
  • Timestamps
  • Safe file resolution

Low-level file writing is in storage.odin.

It provides:

  • Exclusive file creation.
  • Temporary-file replacement.
  • Cleanup after failed writes.

Application-level ownership and switching are in app_project.odin.

Startup loading is coordinated by app_state.odin.

A beginner’s mental model

Think of RAMAL-EBX as a laboratory with three rules:

  1. Every experiment belongs to a named Project.
  2. Every file must stay inside its Project’s filing cabinet.
  3. Every update must leave either a complete old record or a complete new record.

These rules make it safer to close, reopen, copy, rename, and inspect research workspaces.

Conclusion

The Project and Persistent Record Boundary gives RAMAL-EBX a dependable foundation.

You learned that:

  • A Project owns the durable research workspace.
  • A descriptor stores identity and solver configuration.
  • Records use IDs and provenance to explain their relationships.
  • Relative paths are checked before they are resolved.
  • Canonical directory and symlink checks protect the workspace.
  • Staged loading prevents partially loaded Projects.
  • Atomic replacement prevents half-written records.
  • Application preferences are separate from Project data.

With this boundary in place, the application can safely build richer scientific workflows on top of it.

Next, we will look at how geometry is represented inside a Project in Scene and Geometry Model.


Generated by AI Codebase Knowledge Builder