On this page

ResearchLane๏ƒ

Receive this surface through ResearchSet lane operations; use that owning object's public entry point.

Implementation source

class ResearchLane(name: str, *, lane_type: str | None = None, lane_id: str | None = None, created_at: str | None = None, metadata: Dict[str, object] | None = None)[source]๏ƒ
Governance (single-residence law, BUG-048):

Lanes are handed out LIVE as read surfaces. Every mutator on this class is set-internal (underscore-prefixed): residence claims, the journal, snapshots, and persistence emission all live on the owning ResearchSet, so public state change flows through set verbs ONLY. Public callers read; the owning set writes.

One object's line of versions inside the research network.

Purpose:

Hold the ordered, full-object version records that describe how one conceptual object moved through candidate futures. A lane is a graph container, not a runtime surface: lane membership has ZERO runtime footprint (versions live in crystallizer custody; runtime residency is an orthogonal, on-demand act).

Contract:

  • Nodes are full-object records keyed by binding-signature SHA256; diffs are a derived read feature, never storage.

  • One node per SHA per lane; the set-level ResidenceRegistry guarantees one lane per SHA network-wide.

  • The anchor (anchor_lane_id + anchor_spell_id) organizes ancestry onto another lane's node; it never moves content.

  • State machine: open -> joined | archived; both exits are terminal for this container (recovery happens via network restore, which rebuilds containers wholesale).

Registration:

MELDER KERNEL - guarded. Lanes are handed out live as READ surfaces and written only by their owning set; a user never constructs or registers one.

Subsystem Context:

The container tier of the ResearchSet package: ResearchSet owns lanes, lanes hold nodes, and nodes reference custody. It accepts BOTH node families - ResearchNode (one object's versions) and GroupedResearchNode (one subsystem's compositions) - dispatching through the module-level identity helper, so a lane of group nodes is a subsystem's timeline exactly as a lane of spell nodes is an object's.

System Context:

A lane is a graph container with ZERO runtime footprint. Membership does not make anything live, and promotion does not move anything between lanes - "which lane holds this version" and "which version is currently selected" are deliberately independent questions. That separation is what lets research organization be reorganized freely without ever disturbing a running world.

  • Mutating verbs require the open state and raise otherwise.

  • describe() / from_payload() are exact inverses (nodes ride nested payloads).

Threading:

Instance RLock serializes node/anchor/state mutation and reads.

Lifecycle:

Owned by exactly one ResearchSet; cleanup() cleans owned nodes then deletes owned fields; idempotent; lock released last.

AGENT_ACCESS: internal

AGENT_PURPOSE:

access: internal. Governance (single-residence law, BUG-048): Lanes are handed out LIVE as read surfaces. Every mutator on this class is set-internal (underscore-prefixed): residence claims, the journal, snapshots, and persistence emission all live on the owning ResearchSet, so public state change flows through set verbs ONLY. Melder kernel machinery: read it to understand the runtime, do not drive it directly.

cleanup() → None[source]๏ƒ

Clean owned nodes, release fields, and mark the lane cleaned.

Contract:

  • IDEMPOTENT under double-checked locking: _cleaned is tested before and inside the lock.

  • OWNS ITS NODES: every held ResearchNode is cleaned (best-effort, so one failing node cannot strand the rest) before the lane's own fields are dropped. A node's single-residence lane is the thing that cleans it.

  • DELETE-NOT-NULL posture, no tombstones; post-cleanup access raises AttributeError via check_cleaned().

  • The lock is deleted LAST, outside the guarded block.

Returns:

None.

property lane_id: str๏ƒ

Return the stable lane id (ULID).

Contract:

  • Machine identity, fixed at construction and unchanged for the lane's life; distinct from name, which is the human-facing key. Survives a describe/from_payload round trip.

Returns:

Lane id.

Return type:

str

property name: str๏ƒ

Return the human-facing lane name.

Contract:

  • The lookup key the owning set indexes by; unique within that set. Distinct from lane_id, which is the stable machine identity.

Returns:

Lane name.

Return type:

str

property lane_type: LaneType๏ƒ

Return the policy vocabulary word for this lane.

Contract:

  • The TYPE is the policy word; the name is freeform. The only place type is enforced is the set's join gate (lane_type_enforcement), where a type-mixing join needs force=True. Reading it here is always allowed and never gated.

Returns:

development, experiment, production, or test.

Return type:

LaneType

property state: LaneState๏ƒ

Return the current lifecycle state.

Contract:

  • ONE-WAY STATE MACHINE: open -> joined or open -> archived, and never back. Only an open lane accepts new work; joined and archived are terminal read-only containers.

  • joined and archived are distinct terminals: joined means the lane's line was folded into a receiver (see joined_into_lane_id); archived means it was retired in place. Neither loses its held nodes - the records stay readable.

Returns:

open, joined, or archived.

Return type:

LaneState

property tip_spell_id: str | None๏ƒ

Return the newest registered identity in this lane, when any.

Contract:

  • The tip is the LAST entry in registration order, so it tracks the node line, not ancestry. It is None only while the lane is empty.

  • It MOVES BACKWARD on detach: when a join transfers the tail nodes out, the tip becomes the last remaining node (or None if all were taken), so it is not a monotonic high-water mark.

Returns:

Tip SHA256 or None while empty.

Return type:

Optional[str]

property anchor_lane_id: str | None๏ƒ

Return the lane this lane anchors onto, when attached.

Contract:

  • Ancestry is a SINGLE pointer, not a list: a lane anchors onto at most one node in one other lane. None means this lane is a root of its own line.

  • Always moves in lockstep with anchor_spell_id - they are set together and cleared together, so one being None implies the other is too.

Returns:

Anchor lane id or None.

Return type:

Optional[str]

property anchor_spell_id: str | None๏ƒ

Return the node identity this lane anchors at, when attached.

Contract:

  • The specific node within anchor_lane_id this lane hangs from. None exactly when anchor_lane_id is None; the pair is set and cleared atomically.

  • Names a node in the OTHER lane, not in this one - it is the ancestry attach point, not one of this lane's own members.

Returns:

Anchor SHA256 or None.

Return type:

Optional[str]

property joined_into_lane_id: str | None๏ƒ

Return the receiving lane id after a join, when joined.

Contract:

  • None UNTIL A JOIN, and set exactly once when the lane transitions to joined. It is the forwarding pointer that says where this lane's line went.

  • Independent of the ANCHOR pointer: anchoring is ancestry organization on an open lane, joining is a terminal handoff. A lane can be anchored without being joined and vice versa.

Returns:

Receiving lane id or None.

Return type:

Optional[str]

property node_count: int๏ƒ

Return the number of version records held by this lane.

Contract:

  • Counts nodes CURRENTLY held, so it drops when a join detaches the tail out. It is not a lifetime total of everything ever registered here - the journal holds that history.

Returns:

Node count.

Return type:

int

property created_at: str๏ƒ

Return the ISO-8601 UTC creation stamp.

Contract:

  • Always present. On a rebuilt lane it is the ORIGINAL recorded time, not the rebuild time, because from_payload passes the stored stamp through.

Returns:

Creation timestamp.

Return type:

str

get_node(spell_id: str) → ResearchNode[source]๏ƒ

Return the version record for one held identity.

Contract:

  • Returns the LIVE node object, not a copy. The node is immutable, so sharing it is safe, but it is the same object the lane holds.

  • A non-held identity raises KeyError naming the lane, so the return is never None and needs no null check. Use has_node to test membership without catching.

Threading:

Lookup runs under self._lock.

Parameters:

spell_id -- Identity to fetch.

Returns:

Held version record.

Return type:

ResearchNode

Raises:

KeyError -- If the identity is not held here.

has_node(spell_id: str) → bool[source]๏ƒ

Return whether this lane holds one identity.

Contract:

  • Tests CURRENT membership of THIS lane only. It says nothing about whether the identity resides elsewhere in the network - that is the set's residence registry. False here plus a residence answer elsewhere is normal after a join moved the node.

Threading:

Membership test runs under self._lock.

Parameters:

spell_id -- Identity to test.

Returns:

True when held.

Return type:

bool

node_spell_ids() → List[str][source]๏ƒ

Return the held identities in registration order.

Contract:

  • A FRESH list, so mutating it cannot alter the lane's order.

  • REGISTRATION ORDER, oldest first; the last element is the tip.

  • Ids only. Use nodes() when the records are needed - it reads the same order without a second lookup.

Threading:

Materialized under self._lock; a coherent snapshot.

Returns:

Detached ordered identity list.

Return type:

List[str]

nodes() → List[ResearchNode][source]๏ƒ

Return the held version records in registration order.

Contract:

  • A FRESH list of the LIVE node objects, registration order, oldest first. The list is detached (safe to mutate); the nodes inside it are immutable and shared.

  • Positionally aligned with node_spell_ids() for the same lane state, since both walk the same order.

Threading:

Built under self._lock; a coherent snapshot of the current line.

Returns:

Detached ordered node list (nodes are immutable).

Return type:

List[ResearchNode]

clear_anchor() → None[source]๏ƒ

Detach this lane's ancestry anchor.

Contract:

  • OPEN LANES ONLY. A joined or archived lane raises, because ancestry is not reorganized on a terminal container.

  • NOT IDEMPOTENT: clearing a lane that holds no anchor raises rather than returning quietly, so a redundant clear is a caller error.

  • Clears BOTH anchor fields together, restoring the lane to a root of its own line. Content is untouched - only the ancestry pointer goes.

  • This is a set-internal effect at the lane level; the public ResearchSet.detach verb is the journalled path. Clearing here alone does not write a journal entry.

Threading:

The open-check and the clear run under self._lock.

Raises:

RuntimeError -- If the lane is not open, or when no anchor exists.

Returns:

None.

describe() → Dict[str, object][source]๏ƒ

Return a detached, serialization-ready snapshot of this lane.

Contract:

  • THE EXACT INVERSE of from_payload(), capturing the FULL lifecycle state - state, both anchor fields, tip, joined-into pointer - not just contents, so a joined or archived lane round-trips as joined or archived rather than reviving open.

  • NODES RIDE NESTED describe() PAYLOADS in registration order, so the whole node line is embedded; the lane's order and tip are reconstructible from nodes alone.

  • Enum fields are emitted as their .value strings and metadata is deep-copied, so the payload is JSON-safe and fully detached.

Threading:

Assembled under self._lock, so lifecycle fields and the node line are mutually consistent.

Returns:

Plain-value payload (exact from_payload() inverse; nodes ride nested describe() payloads in registration order).

Return type:

Dict[str, object]

classmethod from_payload(payload: Dict[str, object]) → ResearchLane[source]๏ƒ

Rebuild one lane from a describe() payload.

Contract:

  • name, lane_id, and a nodes LIST are the hard requirements; their absence or wrong type raises ValueError. Everything else degrades to a default.

  • NODE-FAMILY DISPATCH per entry: a payload tagged with the grouped node type hydrates as a GroupedResearchNode, an untagged one as a ResearchNode. Back-compat is by ABSENCE - pre-grouping payloads have no tag and correctly rebuild as spell nodes.

  • LANE-TYPE BACK-COMPAT: a payload sealed before the type vocabulary carries no lane_type, and hydrates as development when its name is default, experiment otherwise - mirroring how a fresh lane of each kind is typed.

  • PRESERVES recorded identity and time: the stored lane_id and created_at are passed through, so a rebuilt lane is the same identity it was sealed as.

  • The tip is recomputed as the last node in the rebuilt order rather than trusted from the payload, so order and tip cannot disagree.

  • Runs each node's own from_payload, so a corrupt node payload is rejected by that node's constructor - the lane cannot rebuild a node its own type would refuse.

Threading:

The rebuilt lane is not shared until this returns; nodes are loaded under the new lane's lock.

Parameters:

payload -- Detached payload produced by describe().

Returns:

Reconstructed lane (state, anchor, order, and tip restored).

Return type:

ResearchLane

Raises:

ValueError -- If the payload shape is invalid.

Topic reference ยท Full contents