Documentation
¶
Overview ¶
model.go — T-119 (wave 10), Stage 0: pkg/obj's core types.
Package obj implements /obj, the manipulable-object primitive (obj-00-design.md): every tracked thing is an entity /obj attaches position and containment capability to, never an independent identity of its own (§4). Subjects are addressed via pkg/storage/meta_subject.go's (kind, key) convention, the same reuse loc's own standalone fences settled on (T-127) — format-only validation, consistent with every other primitive's meta-subject addressing in this codebase (none does a live existence check against an entity row; @C04c's engine-inert law extends to the convention even where, as here, the primitive built on top of it is itself guard-bearing).
retire.go — T-122's own minimal, focused build of retire (obj-01-rest-api.md §6, obj-00-design.md §12): built here only because T-122's own filed exit criteria names "one retire" in its fixture, not as a front-run of T-124's own fuller "API surface completion" scope. What ships: Retire itself (irreversible, refuses if the subject still contains anything, journaled). What does NOT ship here, deliberately out of scope for this item: retrofitting Move/Detach/MoveToContainer to refuse further operations against an already-retired subject. A retired subject's row persists (unlike Detach's own deletion) so GET and the journal fold can still see it, matching §12's own "closer in shape to a bal account closure" framing -- but nothing yet stops a caller from calling Move against a retired subject_ref. Flagged here, not silently left as an undocumented gap, and left for T-124 to close properly alongside the other lifecycle-completion work that item already owns.
Index ¶
- type Adapter
- func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
- func (a *Adapter) PostCommit(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Release(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Reserve(ctx context.Context, tenantKey string, op dxp.OpParams, ...) (dxp.Claim, error)
- func (a *Adapter) Validate(ctx context.Context, c dxp.Claim) error
- type AlreadyAttachedError
- type AlreadyRetiredError
- type Capacity
- type CapacityError
- type CapacityInvalidError
- type ContainerNotAttachedError
- type ContainmentCycleError
- type DemoteRefusedError
- type DetachRefusedError
- type DxpAttachAndContainParams
- type DxpDetachParams
- type MoveTarget
- type NotAttachedError
- type Position
- type PositionKind
- type ResolvedPosition
- type RetireRefusedError
- type Store
- func (s *Store) Attach(ctx context.Context, subjectRef string, capacity Capacity) error
- func (s *Store) DB() *sql.DB
- func (s *Store) Detach(ctx context.Context, subjectRef string) error
- func (s *Store) DirectContents(ctx context.Context, containerRef string) ([]string, error)
- func (s *Store) Get(ctx context.Context, subjectRef string) (*Subject, error)
- func (s *Store) Init(ctx context.Context) error
- func (s *Store) List(ctx context.Context) ([]Subject, error)
- func (s *Store) Move(ctx context.Context, subjectRef string, target MoveTarget, locStore *loc.Store) error
- func (s *Store) MoveToContainer(ctx context.Context, subjectRef, containerRef string) error
- func (s *Store) Oracles() []chronicle.RebuildOracle
- func (s *Store) PositionFoldOracle() chronicle.RebuildOracle
- func (s *Store) Report(ctx context.Context, subjectRef string, lat, lon float64, locStore *loc.Store) error
- func (s *Store) ResolvePosition(ctx context.Context, subjectRef string) (ResolvedPosition, error)
- func (s *Store) Retire(ctx context.Context, subjectRef string) error
- func (s *Store) SetCapacity(ctx context.Context, subjectRef string, capacity Capacity) error
- func (s *Store) SetGraph(g objGraph)
- func (s *Store) TenantID() tenant.TenantID
- func (s *Store) TransitiveContents(ctx context.Context, containerRef string) ([]string, error)
- type Subject
- type UnknownSubjectError
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter is obj's dxp.Participant. One Adapter per Store; safe for concurrent use.
func NewAdapter ¶
NewAdapter wires store into cache and returns an Adapter ready to register with a dxp coordinator under the primitive key "obj".
func (*Adapter) Execute ¶
func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
Execute applies the pending operation via the coordinator-supplied tx (dxp-coordinator-design.md §11: one SQL transaction per participant; the coordinator opens and commits tx, never Execute).
func (*Adapter) PostCommit ¶
PostCommit is a safe, cheap no-op for now — T-123's own scope eventually folds the graph mirror in here (obj-00-design.md §10), the identical shape bal.Adapter.PostCommit already established. Exists now so T-123 doesn't need a second interface change, matching dxp.Participant's own doc comment for this verb. PostCommit best-effort mirrors the committed containment change into the live graph (T-123, obj-00-design.md §10) — the identical commit-first-authoritative/mirror-second-best-effort shape bal.Adapter.PostCommit already established, applied to a genuinely new mirror target (no other primitive's own PostCommit touches the graph). Re-reads the pending op's own params rather than trusting any state from Execute — the same discipline bal's own PostCommit doc names explicitly, for the identical reason: PostCommit runs separately, potentially some time after Execute.
func (*Adapter) Release ¶
Release drops txn's stashed params, if any. Idempotent and unconditional, matching loc/bal/cal/entity exactly.
func (*Adapter) Reserve ¶
func (a *Adapter) Reserve(ctx context.Context, tenantKey string, op dxp.OpParams, txn, participantID string, deadline int64, w dxp.Weight) (dxp.Claim, error)
Reserve evaluates admission for whichever op type it's given. For attach-and-contain: the destination container's current count plus every live pessimistic claim against it (this reservation's own included) against its ceiling — the identical "count + claims <= ceiling" arithmetic loc's own Reserve applies to a leaf, applied here to a container. For detach: no capacity to guard, so this always consents at Reserve time — the real checks (still attached, contains nothing) run for real in Execute, against transaction- scoped state, not a pre-transaction snapshot.
type AlreadyAttachedError ¶
type AlreadyAttachedError struct{ SubjectRef string }
AlreadyAttachedError: XOLU-OBJ006 — subject already has obj capability composed. HTTP 409.
func (*AlreadyAttachedError) Error ¶
func (e *AlreadyAttachedError) Error() string
type AlreadyRetiredError ¶
type AlreadyRetiredError struct{ SubjectRef string }
AlreadyRetiredError: retiring a subject that is already retired. Not itself a named XOLU-OBJ code in obj-01-rest-api.md's own §6 — retire's own spec covers only the "still contains something" refusal explicitly — mapped to the same XOLU-OBJ012 bucket at the HTTP layer since both are "retire cannot proceed" refusals of the same shape, not a distinct wire-visible case worth its own code.
func (*AlreadyRetiredError) Error ¶
func (e *AlreadyRetiredError) Error() string
type Capacity ¶
type Capacity struct {
MaxWeightKg *float64
MaxVolumeM3 *float64
MaxCount *int64
CurWeightKg float64
CurVolumeM3 float64
CurCount int64
}
Capacity is a subject's own optional multi-dimensional ceiling (obj-00-design.md §7) — weight, volume, and count are each independently optional; a subject with none set can be positioned and can itself be contained, but cannot hold anything (obj-01-rest- api.md §1). Current* fields are the guard-bearing running totals, mirroring loc_capacity's own count column, generalised to three independent dimensions instead of one.
type CapacityError ¶
CapacityError: XOLU-OBJ003 — destination obj subject at capacity. T-120's own scope only ever sets Dimension "count" — max_weight_kg/ max_volume_m3 enforcement is a deliberately deferred, separate item (see model.go's own Capacity doc comment for why: no field exists yet for what a contained subject itself contributes, and the unit question — kg vs lb, m3 vs L vs ft3 — is genuinely unresolved). HTTP 409.
func (*CapacityError) Error ¶
func (e *CapacityError) Error() string
type CapacityInvalidError ¶
type CapacityInvalidError struct{ SubjectRef string }
CapacityInvalidError: XOLU-OBJ008 — a capacity update leaves every dimension unconstrained (obj-01-rest-api.md §4: "at least one dimension must be set, or the subject cannot hold anything" — matching /loc's own XOLU-LOC011 refusal shape). HTTP 400.
func (*CapacityInvalidError) Error ¶
func (e *CapacityInvalidError) Error() string
type ContainerNotAttachedError ¶
type ContainerNotAttachedError struct{ ContainerRef string }
ContainerNotAttachedError: XOLU-OBJ005 — the target obj subject (the intended container) is not itself obj-attached. HTTP 409, matching obj-01-rest-api.md §2's own refusal list exactly (not 404 — the subject may well exist as an entity, it simply hasn't been given obj capability, a distinct fact from "does not resolve").
func (*ContainerNotAttachedError) Error ¶
func (e *ContainerNotAttachedError) Error() string
type ContainmentCycleError ¶
ContainmentCycleError: XOLU-OBJ004 — the move would create a containment cycle. HTTP 409.
func (*ContainmentCycleError) Error ¶
func (e *ContainmentCycleError) Error() string
type DemoteRefusedError ¶
type DemoteRefusedError struct{ SubjectRef string }
DemoteRefusedError: XOLU-OBJ011 — demote refused because the subject currently contains something of its own (obj-01-rest- api.md §5: "dissolve contents first"). HTTP 409.
func (*DemoteRefusedError) Error ¶
func (e *DemoteRefusedError) Error() string
type DetachRefusedError ¶
type DetachRefusedError struct {
SubjectRef string
Reason string // "positioned" | "occupied" -- which half of the OR fired
}
DetachRefusedError: XOLU-OBJ007 — detach refused because the subject currently contains something or is positioned anywhere other than unassigned (obj-01-rest-api.md §1). HTTP 409.
func (*DetachRefusedError) Error ¶
func (e *DetachRefusedError) Error() string
type DxpAttachAndContainParams ¶
type DxpAttachAndContainParams struct {
SubjectRef string `json:"subject_ref"`
ContainerRef string `json:"container_ref"`
Capacity Capacity `json:"capacity,omitempty"`
}
DxpAttachAndContainParams is obj's dxp.OpParams for promote's own obj leg (obj-01-rest-api.md §5): attach obj capability to SubjectRef and immediately position it as contained by ContainerRef, atomically. SubjectRef is always caller-composed from a PRE-ALLOCATED entity id (storage.SQLiteStore.AllocateNodeID) — promote's own handler resolves this before building the dxp transaction (dxp has no mechanism for one leg's execution result to feed another leg's params, confirmed directly against EntityAdapter.Execute before this shape was settled on).
func (DxpAttachAndContainParams) Primitive ¶
func (DxpAttachAndContainParams) Primitive() string
Primitive satisfies dxp.OpParams.
type DxpDetachParams ¶
type DxpDetachParams struct {
SubjectRef string `json:"subject_ref"`
}
DxpDetachParams is obj's dxp.OpParams for demote's own obj leg: unassign SubjectRef's current position (relinquishing its container's own count, if any) and remove obj capability entirely, atomically. XOLU-OBJ011 if SubjectRef still contains anything.
func (DxpDetachParams) Primitive ¶
func (DxpDetachParams) Primitive() string
Primitive satisfies dxp.OpParams.
type MoveTarget ¶
type MoveTarget struct {
Kind PositionKind
LocLeafID string // set iff Kind == PositionKindLocLeaf
ContainerRef string // set iff Kind == PositionKindObj (Stage 2, T-120)
}
MoveTarget is obj-01-rest-api.md §0's own "to" object — one of three kinds.
type NotAttachedError ¶
type NotAttachedError struct{ SubjectRef string }
NotAttachedError: subject has no obj capability — the target of a GET/detach/move/report against a subject that was never attached, or was already detached. Mapped to XOLU-OBJ001 at the HTTP layer (obj-01-rest-api.md names no separate code for this specific case; "does not resolve" covers it — an obj capability that was never attached is exactly a subject the obj surface doesn't recognise).
func (*NotAttachedError) Error ¶
func (e *NotAttachedError) Error() string
type Position ¶
type Position struct {
Kind PositionKind
LocLeafID string // set iff Kind == PositionKindLocLeaf
ContainedBy string // set iff Kind == PositionKindObj (Stage 2) — the containing subject's own Ref
UpdatedAt time.Time
}
Position is a subject's own recorded position — obj's own canonical record for all three of §6's termination kinds, not a derived read through /loc's tables. This is deliberate, not incidental: /loc has no representation at all for "unassigned" (obj-01-rest-api.md §0's own named gap this design closes) or for "contained by another entity," so obj cannot lean on /loc's own loc_assignment as its single source of truth even for the loc_leaf case — a subject moved to a loc_leaf is authoritative in *both* places: /loc's own guard-bearing leaf-capacity CAS decides whether the move is admitted at all, and obj's own Position row is /obj's independent record of which of its three termination kinds currently applies. See store.go's own MoveToLocLeaf doc comment for the real consequence of this split: two separate SQLite files, two separate commits, named plainly rather than assumed atomic.
type PositionKind ¶
type PositionKind string
PositionKind names which of §6's three termination cases a subject's position currently resolves to at the *first* hop — not the final resolved answer, which may require walking further through other obj subjects (kind == PositionKindObj).
const ( // PositionKindLocLeaf: anchored at a /loc tree leaf. Stage 1. PositionKindLocLeaf PositionKind = "loc_leaf" // PositionKindObj: contained by another /obj subject — this IS // containment (obj-00-design.md §5), not a separate fact. Stage 2. PositionKindObj PositionKind = "obj" // PositionKindUnassigned: explicitly off-site/unknown, a // first-class ordinary state (obj-00-design.md §12), not an error. // Stored as an empty PositionKind, never a Go nil special-case — // the zero value already means this. PositionKindUnassigned PositionKind = "" )
type ResolvedPosition ¶
type ResolvedPosition struct {
Kind PositionKind
LocLeafID string // set iff Kind == PositionKindLocLeaf
Chain []string // subject refs walked, nearest-first, always at least [subjectRef] itself
}
ResolvedPosition is obj-01-rest-api.md §2's own GET .../position response shape, pre-JSON: what the chain terminates at, and how many hops it took to get there. Genuinely multi-hop as of T-120 — walking through several PositionKindObj containment edges before reaching a loc_leaf or unassigned termination.
type RetireRefusedError ¶
type RetireRefusedError struct{ SubjectRef string }
RetireRefusedError: XOLU-OBJ012 — retire refused because the subject currently contains something (obj-01-rest-api.md §6). HTTP 409.
func (*RetireRefusedError) Error ¶
func (e *RetireRefusedError) Error() string
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) Attach ¶
Attach gives an existing entity obj capability (obj-01-rest-api.md §1). XOLU-OBJ006 if already attached. subjectRef must already be format-validated and canonicalised by the caller (the HTTP handler layer, T-127's own precedent).
func (*Store) DB ¶
DB exposes the underlying *sql.DB for callers needing raw access — tests checking a real side effect directly, not ordinary callers.
func (*Store) Detach ¶
Detach removes obj capability entirely (obj-01-rest-api.md §1) — bookkeeping cleanup, not the lifecycle retire operation (§6). XOLU-OBJ007 if the subject is positioned anywhere other than unassigned. The "or currently contains anything" half of §1's OR refusal condition has no table to check against yet in this stage (the containment edge table is Stage 2's own deliverable, obj-02-implementation.md) — named here rather than silently omitted, and Stage 2 must extend this check when that table exists, not treat it as already handled.
func (*Store) DirectContents ¶
DirectContents returns every subject directly contained by containerRef (obj-01-rest-api.md §3's own default, no ?depth=all) — a plain SQL read against obj_position.contains_ref, guard-free, never the graph (obj-00-design.md §10's own guard-locality rule extends to reads too: the graph is a mirror, /obj's own SQL is canonical, and canonical is what an ordinary GET should answer from). Confirms containerRef itself is attached first — an unknown or never-attached container should read as XOLU-OBJ001, not a confusing empty list.
func (*Store) List ¶ added in v0.30.38
List returns every subject currently attached in this tenant -- XOT209 (wave 15): the enumeration this package never had, found missing by reading the full HTTP route table directly (every registered /obj route is a per-id lookup or a write; nothing enumerates). Mirrors Get's own two-query shape (obj_subjects for capacity/retired/created, obj_position for placement) but as a single JOIN rather than N+1 queries, ordered by subject_ref for a stable, diffable result. INNER JOIN deliberately, matching Get's own documented invariant that Attach always creates the position row in the same transaction -- a subject missing its position row would be a bug to surface by omission, not paper over with a LEFT JOIN and a nil check.
func (*Store) Move ¶
func (s *Store) Move(ctx context.Context, subjectRef string, target MoveTarget, locStore *loc.Store) error
Move sets subjectRef's canonical position (obj-01-rest-api.md §2).
For PositionKindLocLeaf, locStore performs /loc's own real guard-bearing leaf-capacity CAS (loc.Store.Move) — that decision is authoritative for leaf occupancy, obj never re-implements it. Named plainly, not glossed over: /loc and /obj live in two separate per-tenant SQLite files (storelayout.TenantLocDir/TenantObjDir), so this is two sequential commits, not one atomic transaction spanning both. If loc's own commit succeeds and obj's own write of obj_position then fails (a narrow window — process crash between the two), obj_position goes stale relative to /loc's own true state until the next successful Move/Report for this subject corrects it. This is the same class of gap dxp's own coordinator exists to close for multi-primitive transactions generally — Stage 1's ordinary REST Move endpoint is not dxp-dispatched, so it does not get that protection. Worth a future item if this proves a real problem in practice; not attempted here.
For PositionKindUnassigned, a REAL, NAMED GAP: if the subject was previously PositionKindLocLeaf, this does NOT vacate its slot in /loc's own leaf-capacity count — /loc has no "move to nothing" primitive, only "move to X", so there is no existing /loc call this function can make to relinquish the leaf. The leaf's occupancy count stays incremented for a subject /obj now considers unassigned. This is a genuine consistency gap between the two primitives for this specific transition, not an oversight papered over — closing it needs either a new /loc capability (a genuine "vacate" verb) or an accepted, documented limitation. Flagged here for the same reason every other real gap this session found got named rather than silently worked around.
func (*Store) MoveToContainer ¶
MoveToContainer sets subjectRef's position to "contained by containerRef" (obj-00-design.md §5, the "obj" kind of §0's own three move targets). Universal cycle safety, guard-bearing, checked in the same transaction as the write — never opt-in (§5's own rejection of the earlier "portable flag" alternative). Also guards containerRef's own max_count dimension in the identical transaction, write-first (see this file's own package doc for why).
XOLU-OBJ004 if the move would create a cycle (self-containment always is, checked before opening a transaction at all — a pure string comparison, no I/O). XOLU-OBJ005 if containerRef is not itself obj-attached. XOLU-OBJ003 if containerRef is at its own count ceiling.
func (*Store) Oracles ¶
func (s *Store) Oracles() []chronicle.RebuildOracle
Oracles returns every fold oracle this store owns — the entry point a rebuild-check caller (package-internal today; iolu's own obj subcommand is wave 6's own job, still 0% built as of this writing, matching loc's identical T-117 note) uses to verify derive(journal) == current.
func (*Store) PositionFoldOracle ¶
func (s *Store) PositionFoldOracle() chronicle.RebuildOracle
PositionFoldOracle: derive(journal) == current for every subject's position — including retired subjects (whose position persists unchanged), excluding detached ones (whose row is gone). Derive is each subject's latest 'attach'/'move' entry, gated on that subject's overall latest journal entry not being 'detach'. Current reads obj_position directly. Both sides fingerprint as sorted "subject_ref position_kind loc_leaf_id_or_dash container_ref_or_dash" lines.
func (*Store) Report ¶
func (s *Store) Report(ctx context.Context, subjectRef string, lat, lon float64, locStore *loc.Store) error
Report mirrors loc's own report/move split (loc-01-rest-api.md §0) for the identical reason: a raw coordinate resolves fence membership only, it never sets Move's own canonical position. For an obj subject, this means routing directly through locStore's own Report — obj adds no logic of its own here, per this stage's own filed scope ("mirrors loc's own directly -- no new logic, just routing through it").
func (*Store) ResolvePosition ¶
ResolvePosition walks a subject's position to its termination (obj-00-design.md §6). Stage 1 handles exactly two of the three cases directly (loc_leaf, unassigned) — the third (contained by another obj subject, walking transitively) is Stage 2's own deliverable; encountering PositionKindObj here is an invariant violation in this stage, since nothing yet writes it. ResolvePosition walks a subject's position to its termination (obj-00-design.md §6): a loc_leaf, unassigned, or (T-120) another obj subject's own position, walked transitively until it reaches one of the first two. The write-time cycle guard (containment.go) makes an infinite walk here impossible for legitimately-written data; chainLimit is a defensive bound against that invariant ever being violated (corruption, a bug), not a normal operating limit — hit only ever means "assert failed," not "grew too complex."
func (*Store) Retire ¶
Retire marks subjectRef permanently retired (obj-00-design.md §12): the physical thing itself has ceased to exist. Irreversible. XOLU-OBJ012 if the subject currently contains anything, or is already retired.
func (*Store) SetCapacity ¶
SetCapacity updates subjectRef's own capacity ceilings (§4). XOLU-OBJ008 if every dimension would end up unset — a subject with no capacity dimension at all cannot hold anything, matching /loc's own "capacity set on a non-postable node" refusal shape.
func (*Store) SetGraph ¶
func (s *Store) SetGraph(g objGraph)
SetGraph attaches the live graph instance obj's own containment writes should best-effort mirror into. Mirrors bal.Store's own SetRollupPebble naming and contract exactly. Never required — obj-00-design.md §10's mirror is advisory throughout; a store that never calls this simply never mirrors, no error, matching every other optional-derived-plane pattern in this codebase.
func (*Store) TransitiveContents ¶
TransitiveContents walks the full containment closure below containerRef (?depth=all) via the identical SQL read DirectContents uses, applied recursively — deliberately NOT the mirrored graph (obj-00-design.md §10 names FindPath/GetNeighbors against the live graph as the closure-query mechanism once mirrored, but the mirror is best-effort and can lag; a subject's own transitive contents, asked of /obj directly rather than of the graph, should answer from the SAME canonical source DirectContents already does, not risk a stale answer from a derived plane for a query this primitive can already serve exactly from its own tables). visited guards against a corrupted/cyclic state defensively — the write-time cycle guard (containment.go) makes a real cycle impossible for legitimately- written data, so hitting the guard here means an invariant was already violated elsewhere, not a normal operating condition.
type Subject ¶
type Subject struct {
Ref string // canonical "kind:key" form, meta_subject.go's own convention
Capacity Capacity
Position Position
RetiredAt *time.Time // T-122: obj-00-design.md §12's terminal state — nil unless retired, once set never cleared
CreatedAt time.Time
}
Subject is one /obj-tracked entity's full row: identity, capacity, and current position — the same "everything about one thing in one struct" shape Location/Fence use in pkg/loc.
type UnknownSubjectError ¶
type UnknownSubjectError struct{ Detail string }
UnknownSubjectError: XOLU-OBJ001 — subject does not resolve to a valid (kind, key) shape. Format-only, per this package's own doc comment in model.go — not a live existence check against an entity row, consistent with every other primitive's meta-subject addressing in this codebase. HTTP 404.
func (*UnknownSubjectError) Error ¶
func (e *UnknownSubjectError) Error() string
type ValidationError ¶
type ValidationError struct{ Detail string }
ValidationError is a generic 400 for malformed input with no XOLU-OBJ code of its own yet — mirrors loc's identical convention and identical reasoning (pkg/loc/errors.go's own doc comment).
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string