recon_gen.common.l2.cache

In-memory L2Instance cache + atomic file-write primitive (X.4.a.6).

Studio’s source-of-truth contract (per SPEC_studio.md): the L2 YAML on disk is the hard truth; the in-memory L2Instance is a cache of the file, never a parallel source. This module ships the two primitives that contract rests on:

  • L2InstanceCache — Studio-owned read cache. Loads once at startup via common/l2/loader.py::load_instance; holds the parsed model so per-request YAML re-parse isn’t on the hot path. Read-only here in X.4.a.6 (get()); the save() method that writes back to disk lands at X.4.d.3 once the editor’s serializer (serialize_l2) is in place. replace(new_instance) is provided now so the X.4.d.1 mutators have a typed seam to update the cache without touching disk.

  • save_yaml_atomic(text, path) — atomic file write: temp file in the same directory, fsync the contents, then rename onto the target. POSIX rename(2) inside one filesystem is atomic, so a crash mid-write leaves either the old YAML or the new YAML on disk — never a torn half-written file. The temp lives in the target’s parent dir specifically so the rename can’t cross filesystems.

Severability: this module is Studio-side. Dashboards (recon-gen dashboards) does NOT instantiate L2InstanceCache — it reads the L2 once at startup and keeps the parsed instance in its own scope. The cache only exists when Studio mounts.

Functions

save_yaml_atomic(text, path)

Atomically write text to path.

Classes

L2InstanceCache(path, instance)

Studio-owned in-memory cache of one L2Instance.

class recon_gen.common.l2.cache.L2InstanceCache(path, instance)[source]

Bases: object

Studio-owned in-memory cache of one L2Instance.

Constructed once at Studio startup via L2InstanceCache.from_path; bound to a single L2 YAML path for its lifetime. Studio request handlers call get() to read the current model; once X.4.d lands its mutators, they call replace(new_instance) to swap the cached value, and X.4.d.3’s serializer-aware save() will pair replace with save_yaml_atomic so disk + cache stay in sync.

Read-only-from-disk for now (X.4.a.6 ships get() + replace); no reload-on-file-change watcher (per the SPEC’s “Studio writes; nobody else writes” rule).

Parameters:
classmethod from_path(path)[source]

Load the YAML at path and wrap the result in a cache.

Return type:

L2InstanceCache

Parameters:

path (Path | str)

get()[source]
Return type:

L2Instance

property path: Path
replace(new_instance)[source]

Swap the cached instance without writing to disk.

X.4.d’s mutators (mutate_l2 / rename_identifier) produce a new L2Instance from the old one; replace installs it. Pairing this with a disk write happens in save() below (which composes serialize_l2 + save_yaml_atomic + replace).

Return type:

None

Parameters:

new_instance (L2Instance)

save(new_instance)[source]

Persist + swap: serialize new_instance to YAML, atomically write it to self.path, then replace the cached instance.

The X.4.e PUT handler composes mutate_l2 validate save — every successful mutation lands on disk + in cache atomically (within one event loop tick). A crash mid-write leaves the prior YAML intact (per save_yaml_atomic’s POSIX rename guarantee).

Caller is responsible for validating new_instance BEFORE calling save — a structural-break delete that re-raises L2ValidationError should never reach disk.

Return type:

None

Parameters:

new_instance (L2Instance)

recon_gen.common.l2.cache.save_yaml_atomic(text, path)[source]

Atomically write text to path.

Writes to a temp file in path.parent (so the final rename stays within one filesystem and is therefore atomic per POSIX rename(2)), fsyncs the file contents, then renames onto the target. A crash between fsync and rename leaves the old file intact; a crash during write leaves a stray .<name>.*.tmp that the next save overwrites.

Caller’s responsibility: path.parent must already exist.

Return type:

None

Parameters:
  • text (str)

  • path (Path)