Documentation
¶
Overview ¶
Package loc is the spatial primitive (wave 9, T-115..T-118): tracking where things are, both as tree-structured containment (a site, a building, a floor, a room, a bin) and as geofenced regions (a yard, a jurisdiction, a service radius). Canonical state is SQL throughout — no bit-packed codec, no Pebble plane — mirroring bal's shape, not cal's or ts's (loc-00-design.md §6a).
Two write paths, not one — read this before touching anything else in this package (loc-01-rest-api.md §0, reproduced here verbatim so this package's own doc states the same contract its wire surface does, not a paraphrase drifting from it over time):
- move — explicit tree-leaf reassignment, by location id. No geometry involved: a leaf's membership in its ancestors is identity (prefix parentage, loc-00-design.md §3a), not a geometric test. This is the only way a subject's canonical tree-leaf position ever changes.
- report — a raw coordinate. Location nodes carry a Placement (loc-00-design.md §3b) — an offset-and-rotation transform, not a boundary shape — so a coordinate cannot be tested against a leaf the way it can against a fence, which does carry real Geometry. report therefore only ever resolves fence membership. It never sets or changes tree-leaf assignment.
A subject can be tree-assigned (via move), fence-tracked (via report), both, or neither. Nothing in this package silently promotes a report into an implicit move, and nothing infers a fence crossing from a move alone unless the destination leaf happens to be tree-aligned with a fence, in which case that fence's membership follows the free tree walk automatically — no separate report needed for that specific fence.
Decisions pinned at Stage 0 (loc-02-implementation.md), so later stages cannot drift from them:
Tenancy follows bal's pattern, not cal's. cal needs a manager type (enable/disable, per-tenant lifecycle) because cal can be absent from a given server instance; nothing about loc names an equivalent reason. A plain per-request store constructor is the default; a manager type is added later only against a concrete need. Storage lives at <data-root>/tNNNN/loc/, a sibling to store/ and ts/ (storelayout.TenantLocDir) — loc's own tables (locations, fences, capacity, journal) are numerous enough to warrant their own file, the same reasoning ts already applies.
Coordinate fields decode as plain typed float64 struct fields, full stop — never through a raw untyped-map decode step or a string intermediate. bal's Amount precedent needed that raw-map step because Amount requires custom decimal parsing; loc's coordinates have no equivalent reason, and a bare JSON number decoded directly into a float64 field is already rejected by encoding/json if malformed, with no literal NaN/Infinity token in the JSON spec to smuggle through. If a string-based coordinate path is ever introduced (a GeoJSON string export/import, say), that path needs an explicit math.IsInf/math.IsNaN guard after strconv.ParseFloat — strconv.ParseFloat accepts "NaN"/"Inf" as valid input even though JSON never permits them as bare tokens. Regression guard: grepping this package for Go's untyped JSON object type outside test files must return nothing.
Client library (pkg/client) and iolu CLI support are explicit v1 non-goals, stated rather than left silent. bal's own client methods shipped as T-67, well after bal itself was solid — the same deferral is correct here.
Index ¶
- type AbsolutePosition
- 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 CapacityError
- type CapacityOnNonPostableError
- type Circle
- type DuplicateFenceError
- type DuplicateLocationError
- type DuplicatePatternError
- type DxpMoveParams
- type FenceDrift
- type FenceKey
- type FenceReconcileResult
- type GeoAnchor
- type Geometry
- type HasChildrenError
- type HistoryEntry
- type InvariantError
- type Location
- type LocationDef
- type LocationKey
- type MoveParams
- type NearbyFence
- type NearbyLocation
- type OccupiedError
- type PatchParams
- type Pattern
- type PatternCapacityConflictError
- type PatternLineage
- type Placement
- type Point
- type Polygon
- type ReportPoint
- type RootAnchorError
- type SelfIntersectingPolygonError
- type Store
- func (s *Store) ApplyFencePattern(ctx context.Context, fenceID, patternID string) (int64, error)
- func (s *Store) ApplyLocationPattern(ctx context.Context, locationID, patternID string) (int64, error)
- func (s *Store) AssignmentFoldOracle() chronicle.RebuildOracle
- func (s *Store) ComposeAbsolutePosition(ctx context.Context, locationID string) (AbsolutePosition, error)
- func (s *Store) CurrentFenceKeys(ctx context.Context, subjectRef string) ([]FenceKey, error)
- func (s *Store) DB() *sql.DB
- func (s *Store) Def(ctx context.Context, def LocationDef) (LocationKey, error)
- func (s *Store) DefFence(ctx context.Context, fenceID string, alignedLocationID *string) (FenceKey, error)
- func (s *Store) DefPattern(ctx context.Context, id string, capacity int64) error
- func (s *Store) Delete(ctx context.Context, locationID string, force bool) error
- func (s *Store) DeletePattern(ctx context.Context, id string) error
- func (s *Store) FenceIDsFor(ctx context.Context, keys []FenceKey) ([]string, error)
- func (s *Store) FenceMembershipFoldOracle() chronicle.RebuildOracle
- func (s *Store) FenceOccupancyFoldOracle() chronicle.RebuildOracle
- func (s *Store) FencePatternLineage(ctx context.Context, fenceKey FenceKey) (*PatternLineage, error)
- func (s *Store) Get(ctx context.Context, locationID string) (*Location, error)
- func (s *Store) GetPattern(ctx context.Context, id string) (*Pattern, error)
- func (s *Store) Init(ctx context.Context) error
- func (s *Store) List(ctx context.Context) ([]*Location, error)
- func (s *Store) ListPatterns(ctx context.Context) ([]Pattern, error)
- func (s *Store) LocationPatternLineage(ctx context.Context, locationKey LocationKey) (*PatternLineage, error)
- func (s *Store) MixedAnchorWarning(ctx context.Context, parentKey *LocationKey, newAnchor *GeoAnchor) (string, error)
- func (s *Store) Move(ctx context.Context, p MoveParams) error
- func (s *Store) Nearby(ctx context.Context, lat, lon, radiusMeters float64) ([]NearbyLocation, []NearbyFence, error)
- func (s *Store) OccupancyFoldOracle() chronicle.RebuildOracle
- func (s *Store) Oracles() []chronicle.RebuildOracle
- func (s *Store) Patch(ctx context.Context, locationID string, p PatchParams) error
- func (s *Store) ReconcileFence(ctx context.Context, fenceID string) (FenceReconcileResult, error)
- func (s *Store) Report(ctx context.Context, subjectRef string, lat, lon float64) error
- func (s *Store) ResolveFenceMembership(ctx context.Context, lat, lon float64) ([]FenceKey, error)
- func (s *Store) SetFenceGeometry(ctx context.Context, fenceID string, geom Geometry) error
- func (s *Store) SubjectHistory(ctx context.Context, subjectRef string, limit int) ([]HistoryEntry, error)
- func (s *Store) SubjectPosition(ctx context.Context, subjectRef string) (SubjectPosition, error)
- func (s *Store) TenantID() tenant.TenantID
- func (s *Store) TreeAlignedFenceDelta(ctx context.Context, subjectRef, toLocationID string) (entered, exited []FenceKey, err error)
- type SubjectPosition
- type UnknownFenceError
- type UnknownLocationError
- type UnknownPatternError
- type UnknownSubjectError
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AbsolutePosition ¶
type AbsolutePosition struct {
Lat, Lon, Alt float64
Heading float64 // radians, absolute (TrueNorth + composed local rotation)
}
AbsolutePosition is a location's fully-resolved real-world position: the placement chain composed from a location up to its nearest georeferenced ancestor, then converted from that ancestor's local frame into WGS84 lat/lon/alt plus an absolute heading.
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter is loc'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 "loc".
func (*Adapter) Execute ¶
func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
Execute applies tp's move via moveInTx against the coordinator- supplied tx (proposal §11: one SQL transaction for every participant; the coordinator opens and commits tx, never Execute).
func (*Adapter) PostCommit ¶
PostCommit is a safe, cheap no-op — loc has no derived/advisory plane fed only by committed writes (no rollup, no occupancy index analogous to cal's H3). Exists so a future one doesn't need a second interface change, matching dxp.Participant's own doc comment for this verb exactly (fsm/entity/ts today are the same shape).
func (*Adapter) Release ¶
Release drops txn's stashed params, if any. Idempotent and unconditional, matching bal/cal exactly. The cache entry itself is removed by the coordinator's ReleaseTxn, not here.
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 whether the destination leaf has room — current count plus every live PESSIMISTIC claim against it (this reservation's own included, once held) against its ceiling — the same "count + claims <= ceiling" arithmetic Stage 2's ordinary CAS applies at commit time, evaluated early here against live reservations rather than committed rows alone. On consent it Holds one claim and stashes tp for Execute. The whole evaluate-then-hold sequence runs under one tenant.Lock/Unlock critical section (proposal §4), matching bal/cal.
func (*Adapter) Validate ¶
Validate re-checks that the sum of every live pessimistic claim against c's leaf — c's own included — still fits its ceiling. That sum already includes c.Amount (1), so this is exactly the invariant Reserve established, re-evaluated against whatever the leaf's count and ceiling are now. The count read and the claims read run under the SAME tenant.Lock, matching bal/cal — a read split across two lock acquisitions would reopen the TOCTOU gap the lock exists to close. Optimistic claims are invisible to guard arithmetic everywhere (bal's §7 doc, generalised) and pass unconditionally.
type CapacityError ¶
CapacityError is returned when a leaf or fence's capacity CAS refuses — the predicate matched zero rows because the entry would exceed the ceiling (bal's §6 pattern, applied to admission instead of bounds). XOLU-LOC002 (leaf) or XOLU-LOC001 (fence).
func (*CapacityError) Error ¶
func (e *CapacityError) Error() string
type CapacityOnNonPostableError ¶
type CapacityOnNonPostableError struct{ LocationID string }
CapacityOnNonPostableError: XOLU-LOC011. HTTP 400.
func (*CapacityOnNonPostableError) Error ¶
func (e *CapacityOnNonPostableError) Error() string
type Circle ¶
Circle is a centre point plus a radius. GeoJSON has no native circle type (loc-00-design.md §4d); loc represents one as its own typed field, never forced into a GeoJSON polygon approximation on the wire.
func (Circle) BoundingBox ¶
BoundingBox approximates the circle's box via the same flat-Earth degrees-per-metre conversion placement.go already uses (loc-00- design.md §4e's accepted precision at this scale) — a cheap, slightly-conservative box; exact edge cases are what the R-tree pre-filter narrows, never decides (§7b, guard-locality).
type DuplicateFenceError ¶
type DuplicateFenceError struct{ FenceID string }
DuplicateFenceError: XOLU-LOC015 — fence_id already defined. HTTP 409.
func (*DuplicateFenceError) Error ¶
func (e *DuplicateFenceError) Error() string
type DuplicateLocationError ¶
type DuplicateLocationError struct{ LocationID string }
DuplicateLocationError: XOLU-LOC014 — location_id already defined. HTTP 409. Found by adversarial testing, not written to spec: no XOLU-LOC code in loc-01-rest-api.md's own table covers this case, and bal's own DefineAccount has the identical gap (a UNIQUE constraint violation surfaces as a raw driver error, mapped to 500 by the default case) — checked directly, not assumed unique to this package. Fixed here, flagged as a likely systemic gap rather than silently fixed in isolation.
func (*DuplicateLocationError) Error ¶
func (e *DuplicateLocationError) Error() string
type DuplicatePatternError ¶
type DuplicatePatternError struct{ PatternID string }
DuplicatePatternError: XOLU-LOC023 — pattern_id already defined, T-131. Same systemic gap T-125 found and fixed for location_id/ fence_id/account_id, closed here proactively rather than reproduced a third time. HTTP 409.
func (*DuplicatePatternError) Error ¶
func (e *DuplicatePatternError) Error() string
type DxpMoveParams ¶
type DxpMoveParams struct {
SubjectRef string `json:"subject_ref"`
ToLocationID string `json:"to_location_id"`
}
DxpMoveParams is loc's dxp.OpParams (T-118, wave 9): reassigns a subject's tree-leaf position via dxp. Deliberately narrower than admission.go's own MoveParams: no caller-supplied fence keys. A real caller manually specifying fence deltas was only ever Stage 2's own test hook — Stage 3 (T-116) built real fence-membership resolution from geometry, and a dxp wire surface should not reintroduce the thing that superseded.
Tree-aligned fence membership (loc-01-rest-api.md §0: "follows the free tree walk automatically") is now handled automatically — Move itself auto-derives entered/exited from tree alignment whenever the caller doesn't explicitly supply fence keys (Stage 6), and Execute's call into moveInTx never does, so a dxp-triggered move gets real tree-aligned fence capacity guards for free, not just leaf capacity. What's still not covered: a self-anchored or standalone fence's membership, which only report's exact geometric test resolves — a dxp move was never going to touch those anyway, since move never resolves a coordinate (§0's own two-write-path distinction).
func (DxpMoveParams) Primitive ¶
func (DxpMoveParams) Primitive() string
Primitive satisfies dxp.OpParams.
type FenceDrift ¶
type FenceDrift struct {
SubjectRef string
Recorded string // always "member" -- only current members are checked
Observed string // "outside_new_boundary" or "no_report_point" (defensive: shouldn't happen, membership is only ever set via Report)
}
FenceDrift names one member whose last-known point no longer falls inside its fence's *current* geometry — the only direction this reconcile can detect, matching loc-01-rest-api.md §2b's own scope: it re-tests recorded members, it never scans for new ones a boundary expansion might now include (that would need a global subject scan, out of scope here).
type FenceKey ¶
type FenceKey uint32
FenceKey is the internal, dense, engine-only identifier for a fence — the same two-identity split LocationKey gets. Stage 2 (T-115) introduces it as a bare identity plus a capacity row; Stage 3 (T-116) adds the real Geometry a fence tests membership against.
type FenceReconcileResult ¶
type FenceReconcileResult struct {
RecordedCount int
ObservedCount int
Drift []FenceDrift
}
FenceReconcileResult is §2b's advisory drift view.
type GeoAnchor ¶
type GeoAnchor struct {
Lat, Lon, Alt float64 // WGS84
TrueNorth float64 // radians, orientation of this node's local axes
}
GeoAnchor georeferences a node — where a subtree meets the real world, exactly IFC's IfcSite. Most nodes carry no anchor; their placement is relative only.
type Geometry ¶
type Geometry interface {
Contains(lat, lon float64) bool
Distance(lat, lon float64) float64 // to boundary, metres
BoundingBox() (minLat, minLon, maxLat, maxLon float64)
}
Geometry is the shape a fence tests containment against. Circle and Polygon are the two implementations (loc-00-design.md §4a); their Contains/Distance/BoundingBox methods land in geometry.go (Stage 3, T-116) — declared here only as types, not yet satisfying this interface, so Stage 0 does not carry Stage 3's own risk.
type HasChildrenError ¶
type HasChildrenError struct{ LocationID string }
HasChildrenError: XOLU-LOC013 — delete refused, children present and force was not set. HTTP 409.
func (*HasChildrenError) Error ¶
func (e *HasChildrenError) Error() string
type HistoryEntry ¶
type HistoryEntry struct {
At string
Kind string
From *string // move only
To *string // move only
Entered []string // report only (or a tree-aligned move, which also carries fence deltas)
Exited []string
}
HistoryEntry is one loc_journal row, resolved back to external ids — the two-identity split applied to history reads exactly as to every other response.
type InvariantError ¶
type InvariantError struct {
Detail string
}
InvariantError marks an impossible state: an exit CAS found no matching row with count > 0, meaning bookkeeping was already wrong before this call — asserted, not silently ignored, the same fsck-style treatment dxp gives its own impossible "abandoned-dirty" case (docs/RESOLVED.md, XOLU-DXP010's own doc comment).
func (*InvariantError) Error ¶
func (e *InvariantError) Error() string
type Location ¶
type Location struct {
Key LocationKey
ID string // external, namespaced, stable
ParentKey *LocationKey
Name string
Postable bool // leaf: subjects can be placed here. false: interior summary node, occupancy is a derived rollup of the subtree only.
Placement
CreatedAt time.Time
}
Location is one node in the containment tree (loc-00-design.md §3a, adopted from bal §3a outright) plus its Placement (§3b). Root nodes (ParentKey == nil) must carry a non-nil Placement.Anchor — enforced at write time (XOLU-LOC010) in store.go, not left to a SQL CHECK constraint, since the rule spans a whole struct, not one column.
type LocationDef ¶
type LocationDef struct {
ID string
ParentID *string // external id of the parent, nil for a root
Name string
Postable bool
Placement Placement
}
LocationDef is the input to Store.Def — everything a caller supplies to create a location node. Key is assigned by the store (dense MAX+1 allocation, mirroring bal.DefineAccount), never caller-chosen.
type LocationKey ¶
type LocationKey uint32
LocationKey is the internal, dense, engine-only identifier for a location node — never on any wire struct (the two-identity split, loc-00-design.md §11a, mirroring bal's AccountKey/account_id pattern exactly). The external identifier is LocationID, a namespaced string.
type MoveParams ¶
type MoveParams struct {
SubjectRef string
ToLocationID string
EnteredFenceKeys []FenceKey
ExitedFenceKeys []FenceKey
}
MoveParams is Move's input. EnteredFenceKeys/ExitedFenceKeys are a membership *delta*, supplied directly by the caller — Stage 2's own "test hook" (loc-02-implementation.md): Move's job is applying the CAS guards correctly given a membership delta, not computing that delta from geometry. Stage 3 (T-116) replaces the caller that computes the delta from real Contains tests; Move's own logic here does not change when that happens.
type NearbyFence ¶
type NearbyLocation ¶
type OccupiedError ¶
type OccupiedError struct{ LocationID string }
OccupiedError: XOLU-LOC012 — delete refused, the node (or a descendant) currently holds an assigned subject, unconditionally, regardless of force (loc-01-rest-api.md §1's own "no flag overrides it" rule). HTTP 409.
func (*OccupiedError) Error ¶
func (e *OccupiedError) Error() string
type PatchParams ¶
type PatchParams struct {
Name *string
Placement *Placement
Ceiling **int64 // nil: no change. non-nil pointing at nil: clear the ceiling (unlimited). non-nil pointing at a value: set it.
}
PatchParams: name, placement, and capacity are mutable (loc-01-rest-api.md §1). postable and the tree position are not — "changing whether a node can hold subjects, or where it sits in the tree, is a structural move with its own admission questions... deliberately does not open in v1." Capacity is *ceiling*, XOLU-LOC011 if set on a non-postable node.
type Pattern ¶
Pattern is a fence-or-location capacity default, addressed by its own caller-chosen id — the same declare-at-known-id convention location_id/fence_id already use, not a separate auto-assigned key.
type PatternCapacityConflictError ¶
type PatternCapacityConflictError struct{}
PatternCapacityConflictError: XOLU-LOC022 — a def/attach set both inline capacity and a pattern reference, T-131. Mirrors obj-01-rest-api.md's XOLU-OBJ013 shape exactly. HTTP 400.
func (*PatternCapacityConflictError) Error ¶
func (e *PatternCapacityConflictError) Error() string
type PatternLineage ¶
FenceOrLocationPatternInfo is the read-side shape §2a's own GET response needs: pattern (the id, echoing what was supplied), pattern_id (the same value again, under the "_id" naming this package's other identity fields use), and a computed pattern_deleted — recomputed on every read, never cached or stored, per §5c's own recompute-and-compare precedent this mechanism reuses directly.
type Placement ¶
type Placement struct {
OffsetX, OffsetY, OffsetZ float64 // relative to parent's frame; metres
Rotation float64 // radians, about Z
Anchor *GeoAnchor
}
Placement is a location node's transform relative to its parent's frame — never a disconnected coordinate of its own (loc-00-design.md §3b, adopting IFC's IfcLocalPlacement model outright). A node's absolute position is composing its placement chain up to the nearest ancestor carrying a non-nil Anchor.
type Point ¶
type Point struct {
Lat, Lon float64
}
Point is one vertex of a Polygon, or a raw reported coordinate — always a bare typed float64 pair, never decoded through an untyped JSON object or string intermediate (Stage 0's pinned JSON-decode discipline, doc.go).
type Polygon ¶
type Polygon struct {
Vertices []Point
}
Polygon is an ordered, simple (non-self-intersecting) vertex list — square, rectangle, triangle, and irregular perimeter are all polygons with different vertex counts, not separate types (loc-00-design.md §4a). Self-intersecting input is rejected at write time (XOLU-LOC020); enforcement lands in geometry.go, Stage 3.
func DecodeGeoJSONPolygon ¶
DecodeGeoJSONPolygon parses a GeoJSON (RFC 7946 §3.1.6) Polygon's exterior ring into this package's own Polygon type. GeoJSON coordinate pairs are [longitude, latitude] — the OPPOSITE order from this package's own Point{Lat, Lon} — a deliberate point of friction named here so it is checked once, in one place, rather than risked at every call site. Holes (interior rings, coordinates[1:], RFC 7946 §3.1.6: "any others MUST be interior rings") are refused outright, not silently dropped — an earlier version took only coordinates[0] and never even looked at any further rings, so a caller submitting a fully RFC-compliant Appendix-A.3-style "with holes" polygon got a fence that silently omitted the hole rather than an error explaining why: the hole area would incorrectly read as "inside" the fence. loc-00- design.md's own decision that holes are unsupported (a different, unneeded shape for this package's scope) was always the design; this fix makes the implementation actually enforce it. A GeoJSON ring's closing vertex (first == last, RFC 7946 §3.1.6's own closure requirement) is dropped on decode: this package's own Polygon is an open vertex list, the edge back to Vertices[0] is implicit (geometry.go's own Contains/SelfIntersects both close the loop via modulo indexing).
func (Polygon) BoundingBox ¶
func (Polygon) Contains ¶
Contains answers point-in-polygon via ray-casting (the even-odd rule, PNPOLY's own reference shape) — correct on concave perimeters without decomposition (loc-00-design.md §4b), never triangulation. Falls through to isAxisAlignedRectangle's O(1) check first (§4c): probably the commonest real shape (yards, parking lots, warehouse zones), and a bounding-box test is exact for a true rectangle, not an approximation.
func (Polygon) Distance ¶
Distance to the polygon's boundary: the minimum point-to-segment distance over every edge, converted to metres via the same flat-Earth approximation used throughout this package at this scale. Correct whether the point is inside or outside — the interface contract is distance-to-boundary.
func (Polygon) EffectiveVertexCount ¶
EffectiveVertexCount counts distinct consecutive vertices, treating RFC 7946's closing repeat of the first vertex (loc-00-design.md §4b's own decode discipline always produces one) as not a distinct point, and collapsing any other adjacent duplicate too — a caller submitting the same point twice in a row shouldn't inflate the count. A simple polygon needs at least 3 to mean anything.
func (Polygon) IsDegenerate ¶
IsDegenerate reports whether this polygon has effectively zero area or fewer than three effective vertices — a legal but useless fence (loc-01-rest-api.md §2's own warnings field, T-132: never a hard refusal, since a degenerate fence someone can never enter is legitimate, just probably not what the caller intended).
func (Polygon) SelfIntersects ¶
SelfIntersects reports whether any two non-adjacent edges of the polygon cross — the write-time check behind XOLU-LOC020 (loc-00-design.md §4b: the same simple-polygon restriction SQLite's own Geopoly extension imposes, not a loc-specific inconvenience).
type ReportPoint ¶
type ReportPoint struct {
Lat, Lon, Alt float64
}
type RootAnchorError ¶
type RootAnchorError struct{ LocationID string }
RootAnchorError: XOLU-LOC010 — a root location was defined or patched without a placement anchor. HTTP 400.
func (*RootAnchorError) Error ¶
func (e *RootAnchorError) Error() string
type SelfIntersectingPolygonError ¶
type SelfIntersectingPolygonError struct{ FenceID string }
SelfIntersectingPolygonError: XOLU-LOC020. HTTP 400.
func (*SelfIntersectingPolygonError) Error ¶
func (e *SelfIntersectingPolygonError) Error() string
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is loc's SQL plane. Canonical state is SQL throughout — no bit-packed codec, no Pebble plane (loc-00-design.md §6a) — mirroring bal's shape, not cal's or ts's. Unlike bal, which shares the tenant's primary store file and needs "t0000_"-style table-name prefixing to avoid collision with other primitives' tables in that same file, loc gets its own dedicated per-tenant SQLite file (storelayout.TenantLocDir, Stage 0's own pinned decision) — the db handle passed to NewStore is expected to already be scoped to that file, so table names here are bare, not prefixed.
func NewStore ¶
NewStore wraps a db handle already opened against loc's own per-tenant SQLite file (storelayout.TenantLocDir). tenantID is kept for TenantID() — the dxp adapter (Stage 5, T-118) needs a cross-primitive-comparable tenant key, the same reason bal.Store keeps its own tenantID despite deriving its table prefix from it.
func (*Store) ApplyFencePattern ¶
ApplyFencePattern is ApplyLocationPattern's fence-shaped sibling. XOLU-LOC004 if fenceID is unknown, XOLU-LOC024 if patternID is unknown.
func (*Store) ApplyLocationPattern ¶
func (s *Store) ApplyLocationPattern(ctx context.Context, locationID, patternID string) (int64, error)
ApplyLocationPattern clones patternID's current capacity onto an already-`Def`d location, recording the lineage pointer — one-time, at creation, never re-applied later (loc-00-design.md §5d's own "changing a pattern later never retroactively touches already- cloned" rule). XOLU-LOC003 if locationID is unknown, XOLU-LOC024 if patternID is unknown, XOLU-LOC011 if the location is non-postable (the same rule an inline capacity already enforces — a pattern is not a way around it).
func (*Store) AssignmentFoldOracle ¶
func (s *Store) AssignmentFoldOracle() chronicle.RebuildOracle
AssignmentFoldOracle: derive(journal) == current for leaf assignment. Derive is each subject's most recent 'move' entry (ROW_NUMBER partitioned by subject_ref, ordered by entry_id desc); Current reads loc_assignment directly. Both sides fingerprint as sorted "subject_ref location_key" lines.
func (*Store) ComposeAbsolutePosition ¶
func (s *Store) ComposeAbsolutePosition(ctx context.Context, locationID string) (AbsolutePosition, error)
ComposeAbsolutePosition resolves a location's absolute real-world position by walking ParentKey from locationID up to the nearest ancestor carrying a non-nil Anchor, composing every hop's Placement along the way (loc-00-design.md §3b), then converting the composed local offset into WGS84 via the flat-Earth approximation §4e already accepts at this scale.
The walk is guaranteed to terminate at an anchor, never at a nil ParentKey with no anchor, because every root is required to carry one (XOLU-LOC010, enforced at Def/Patch time) — a malformed tree reaching a nil-parent node with no anchor is an invariant violation, reported as an error here rather than assumed impossible and left to panic.
func (*Store) CurrentFenceKeys ¶
CurrentFenceKeys returns a subject's current fence membership — loc_fence_membership read directly, the same live-tracked-not- derived state Report itself maintains (Stage 3).
func (*Store) DB ¶
DB exposes the underlying *sql.DB for callers that need raw access to loc's own tables — tests checking a real side effect directly, matching the hotel test's own "raw SQL against cal_bookings" discipline (v2_dxp_hotel_test.go), not a derived read path. Not meant for ordinary callers, who should use Store's own methods.
func (*Store) Def ¶
func (s *Store) Def(ctx context.Context, def LocationDef) (LocationKey, error)
Def creates a location node. Mirrors bal.DefineAccount's shape: dense MAX(key)+1 allocation inside the same transaction as the insert, external id supplied by the caller and never reused. Def creates a location node. Mirrors bal.DefineAccount's shape: dense MAX(key)+1 allocation, external id supplied by the caller and never reused — but write-first, unlike bal's own version and this function's own earlier form: the allocation and the parent_id resolution both happen INSIDE one INSERT...SELECT...RETURNING statement, the transaction's opening write, rather than a preceding SELECT. An early version did the allocation as a separate read first — a sandbox adversarial concurrency test (30 goroutines defining distinct locations) surfaced real, if intermittent, SQLITE_BUSY failures before this fix, the same WAL read-then-write- upgrade class T-115 already found and fixed in Move; not assumed safe here by analogy, confirmed directly with the same kind of test.
func (*Store) DefFence ¶
func (s *Store) DefFence(ctx context.Context, fenceID string, alignedLocationID *string) (FenceKey, error)
DefFence creates a fence identity plus its loc_fence_capacity row (ceiling NULL, count 0), the same paired-insert-in-one-tx shape Def uses for locations. Stage 2 gives fences no geometry at all — only identity and capacity — since fence membership is a caller- supplied test hook here (Move's EnteredFenceKeys/ExitedFenceKeys), not yet computed from real Contains tests (Stage 3, T-116). DefFence creates a fence identity plus its loc_fence_capacity row (ceiling NULL, count 0), the same paired-insert-in-one-tx shape Def uses for locations. alignedLocationID, if non-nil, marks this as a tree-aligned fence (loc-01-rest-api.md §2): its identity is still fence_id (kept as the internal addressing key throughout this package), but Move's own ancestor walk (admission.go) uses aligned_location_key to auto-derive entered/exited fences for a tree-assigned subject — the "free tree walk" loc-00-design.md §5 describes, not requiring the exact Contains test §7b reserves for a guard decision on a genuinely reported coordinate. DefFence creates a fence identity plus its loc_fence_capacity row (ceiling NULL, count 0), the same paired-insert-in-one-tx shape Def uses for locations. alignedLocationID, if non-nil, marks this as a tree-aligned fence (loc-01-rest-api.md §2): its identity is still fence_id (kept as the internal addressing key throughout this package), but Move's own ancestor walk (admission.go) uses aligned_location_key to auto-derive entered/exited fences for a tree-assigned subject — the "free tree walk" loc-00-design.md §5 describes, not requiring the exact Contains test §7b reserves for a guard decision on a genuinely reported coordinate.
Write-first, same fix and same reason as Def: the dense-key allocation and the aligned_location_key resolution both happen inside one INSERT...SELECT...RETURNING statement rather than a preceding SELECT — an early version had the identical read-first race Def's own adversarial concurrency test caught, fixed here alongside it rather than left for a later session to rediscover.
func (*Store) DefPattern ¶
DefPattern creates a pattern. XOLU-LOC023 if the id is already defined — write-first (INSERT...RETURNING, the sqliteConstraintUnique check on the actual write), not a read-first existence check, for the identical WAL-race reason Def/DefFence/DefineAccount already learned this the hard way (T-115, T-125).
func (*Store) Delete ¶
Delete removes a location. Refuses (XOLU-LOC012) unconditionally — regardless of force — if the node or any descendant currently holds an assigned subject (loc-01-rest-api.md §1: "silently vacating a subject's canonical position is a correctness violation, not a convenience, and no flag overrides it"). Otherwise refuses (XOLU-LOC013) if the node has children, unless force is set, which cascades to remove *empty* descendants only — safe once the occupied check above has already run, since force can no longer reach an occupied node by the time it executes.
func (*Store) DeletePattern ¶
DeletePattern removes a pattern definition outright — no cascade refusal, mirroring obj-01-rest-api.md §4a's own DELETE exactly: already-cloned fences/locations keep their own snapshotted capacity regardless; only their next GET's computed pattern_deleted reflects the change. XOLU-LOC024 if unknown (matches DELETE's usual not-found shape elsewhere in this package, e.g. UnknownFenceError).
func (*Store) FenceIDsFor ¶
FenceIDsFor resolves internal fence keys to their external fence_id strings — the two-identity split (§11a) applied to Stage 6's own response building: entered/exited/fences in every HTTP response are always fence_id, never a FenceKey. Order-preserving (matches the order keys were supplied in, not a set), and empty input returns an empty, non-nil slice — every wire response wants `[]`, never `null`, for an empty fence list.
func (*Store) FenceMembershipFoldOracle ¶
func (s *Store) FenceMembershipFoldOracle() chronicle.RebuildOracle
FenceMembershipFoldOracle: derive(journal) == current for fence membership, extending Stage 4's own leaf-shaped pattern to the fence-shaped state Stage 3 (T-116) introduced — the plan's own SQL targets cover leaf assignment/occupancy explicitly; this is the same discipline applied to loc_fence_membership, not a narrower verification story for fences than for leaves.
func (*Store) FenceOccupancyFoldOracle ¶
func (s *Store) FenceOccupancyFoldOracle() chronicle.RebuildOracle
FenceOccupancyFoldOracle: derive(journal) == current for fence capacity counts, the fence-shaped counterpart to OccupancyFoldOracle.
func (*Store) FencePatternLineage ¶
func (s *Store) FencePatternLineage(ctx context.Context, fenceKey FenceKey) (*PatternLineage, error)
FencePatternLineage is LocationPatternLineage's fence-shaped sibling.
func (*Store) GetPattern ¶
GetPattern fetches one pattern. XOLU-LOC024 if unknown.
func (*Store) Init ¶
Init creates loc's tables. Idempotent. Stage 1's locations table, plus Stage 2's capacity/assignment/journal tables — fences here is a bare identity (fence_key, fence_id) only; Stage 3 (T-116) adds the real geometry columns via its own Init addition, not a rewrite of this one.
func (*Store) List ¶
List returns every location, ordered by key (stable, insertion order). No pagination in Stage 1 — the same v1-scoping precedent bal and cal both set for their own early stages; added later only against a concrete need.
func (*Store) ListPatterns ¶
ListPatterns returns every pattern, ordered by id for a stable listing (patterns have no dense internal key the way locations/ fences do — pattern_id is the only identity there is).
func (*Store) LocationPatternLineage ¶
func (s *Store) LocationPatternLineage(ctx context.Context, locationKey LocationKey) (*PatternLineage, error)
LocationPatternLineage returns nil when the location was never cloned from a pattern — the common case, and the response builder's own signal to omit all three pattern fields entirely rather than emit them null.
func (*Store) MixedAnchorWarning ¶
func (s *Store) MixedAnchorWarning(ctx context.Context, parentKey *LocationKey, newAnchor *GeoAnchor) (string, error)
MixedAnchorWarning checks a location's own newly-set anchor against the nearest already-anchored ancestor, per loc-01-rest-api.md §1's own warnings field (T-132). Empty string, nil error means nothing to warn about — either no anchor was set, or no ancestor has one to compare against, or the distance is within the plausible-single- tree threshold.
func (*Store) Nearby ¶
func (s *Store) Nearby(ctx context.Context, lat, lon, radiusMeters float64) ([]NearbyLocation, []NearbyFence, error)
Nearby answers "what's near this point" (loc-01-rest-api.md §4) — a read convenience, never a guard input (§7d): advisory distance ordering, not a correctness-bearing containment test the way ResolveFenceMembership is. Locations: every postable leaf's resolved absolute position (ComposeAbsolutePosition, placement.go) within radius, sorted nearest first — a full scan, not R-tree pre-filtered, since locations carry no bounding-box index the way fences do (a real v1 simplicity, acceptable for the "hundreds to low thousands" scale this package targets, per loc-00-design.md §6's own scale statement). Fences: the same R-tree pre-filter ResolveFenceMembership uses, widened by radius, then the exact Distance() test for true ordering.
func (*Store) OccupancyFoldOracle ¶
func (s *Store) OccupancyFoldOracle() chronicle.RebuildOracle
OccupancyFoldOracle: derive(journal) == current for leaf occupancy counts. Derive folds the same "last move per subject" view as AssignmentFoldOracle, grouped by destination leaf; Current reads loc_capacity.count. Locations with zero derived occupants are omitted from Derive symmetrically with Current (bal's own GlobalFoldOracle documents the identical convention) — a never-occupied or fully-vacated location has no row on either side, not a "0" row on one side and an absent row on the other.
func (*Store) Oracles ¶
func (s *Store) Oracles() []chronicle.RebuildOracle
Oracles returns every rebuild oracle this package defines — the hook point for iolu db check (loc-02-implementation.md Stage 4), matching ts/cal/bal's own oracle-registration shape. iolu itself (wave 6) is still 0% built as of this writing, so this is the hook only, not a shipped CLI surface, per Stage 0's own decision to keep iolu wiring out of scope.
func (*Store) Patch ¶
Patch updates a location's mutable fields in place. XOLU-LOC003 if the location doesn't exist; XOLU-LOC011 if Ceiling is set on a non-postable node.
func (*Store) ReconcileFence ¶
ReconcileFence re-tests every subject currently recorded in loc_fence_membership for fenceID against the fence's *current* geometry — §5c of loc-00-design.md, chronicle.RebuildOracle-shaped in spirit (derive fresh, compare to current, surface disagreement) though not a literal instantiation of that type, since the useful output here is a structured per-subject drift list, not a single canonical-string fingerprint. Read-only: never writes loc_fence_capacity.count or loc_fence_membership, both guard-bearing — an advisory view exists precisely so a derived process never touches guard-bearing state outside the ordinary CAS path (§5c's own rule, T-130's filed exit criteria). Reuses the bounding-box-free exact Contains test directly, not the rtree pre-filter — the candidate set here is already exactly known (loc_fence_membership's own rows for this fence), so there's nothing to pre-filter.
func (*Store) Report ¶
Report resolves a raw coordinate's fence membership and nothing else — it never sets or changes tree-leaf assignment (the two-write- path distinction, doc.go / loc-01-rest-api.md §0). The delta against the subject's previously-known membership (loc_fence_membership, tracked directly rather than derived, the same shape loc_assignment gives leaf position) is applied through the identical CAS guards Move uses for fences — a report that would exceed a fence's capacity is refused exactly like a move would be, per §5a's capacity guard being resolved identically for report and move.
func (*Store) ResolveFenceMembership ¶
a raw coordinate — the two-stage design §6b describes, already built into the pinned SQLite dependency: fences_rtree narrows candidates by bounding-box overlap (cheap), then each candidate's real Contains test decides membership exactly (never the pre-filter's cached box alone, §7b's guard-locality rule — a box overlap is necessary but not sufficient for true containment).
func (*Store) SetFenceGeometry ¶
SetFenceGeometry validates and stores a fence's real shape, replacing Stage 2's test hook — this is what "report end-to-end resolves real fence membership through real geometry" (Stage 3's own exit criterion) actually wires up. Self-intersection is rejected (XOLU-LOC020) before anything is written. The bounding box is computed once here and stored both in fences (source of truth) and fences_rtree (the pre-filter index) — kept in sync in the same transaction, never allowed to drift apart.
func (*Store) SubjectHistory ¶
func (s *Store) SubjectHistory(ctx context.Context, subjectRef string, limit int) ([]HistoryEntry, error)
SubjectHistory returns a subject's movement journal, newest first, limited to at most limit rows — loc-01-rest-api.md §3's own pagination contract; this package's own v1 scope stops at a single page (no cursor support yet), the same v1 non-goal boundary this project draws elsewhere rather than half-building pagination.
func (*Store) SubjectPosition ¶
SubjectPosition resolves current canonical state directly from loc_assignment (leaf), loc_fence_membership (fences), and loc_journal's own most recent report row (last_report_point) — never re-derived from a fold, matching this package's own §8a distinction between live-tracked current state and the rebuild oracle's separate (Stage 4) verification role.
func (*Store) TreeAlignedFenceDelta ¶
func (s *Store) TreeAlignedFenceDelta(ctx context.Context, subjectRef, toLocationID string) (entered, exited []FenceKey, err error)
TreeAlignedFenceDelta computes the entered/exited tree-aligned fence sets a move from the subject's current position to toLocationID would produce — the symmetric difference between the destination's own ancestor-chain fences and the origin's. Plain autocommit reads, run BEFORE any transaction opens: safe from the WAL read-then-write upgrade problem moveInTx's own write-first shape exists to avoid (this isn't a read inside a transaction that later writes — it's a read with no transaction at all, resolved once, before one starts).
type SubjectPosition ¶
type SubjectPosition struct {
Leaf *string
Fences []string
LastReportPoint *ReportPoint
AsOf *string // RFC3339; nil only when Leaf and LastReportPoint are both nil
}
SubjectPosition is the canonical-state response loc-01-rest-api.md §3's GET .../position describes: leaf is nil for a subject only ever report-tracked, LastReportPoint is nil for one only ever moved — both nil is a subject never referenced by either verb.
type UnknownFenceError ¶
type UnknownFenceError struct{ FenceID string }
UnknownFenceError: XOLU-LOC004. HTTP 404.
func (*UnknownFenceError) Error ¶
func (e *UnknownFenceError) Error() string
type UnknownLocationError ¶
type UnknownLocationError struct{ LocationID string }
UnknownLocationError: XOLU-LOC003. HTTP 404.
func (*UnknownLocationError) Error ¶
func (e *UnknownLocationError) Error() string
type UnknownPatternError ¶
type UnknownPatternError struct{ PatternID string }
UnknownPatternError: XOLU-LOC024 — pattern_id does not resolve, T-131 (GET/patterns/{id}, or a def/attach referencing a nonexistent pattern). HTTP 404.
func (*UnknownPatternError) Error ¶
func (e *UnknownPatternError) Error() string
type UnknownSubjectError ¶
type UnknownSubjectError struct{ Detail string }
UnknownSubjectError: XOLU-LOC005 — a fence's subject reference does not resolve, T-127. In practice this means the (kind, key) shape itself is invalid (unknown kind, malformed key, or a subject that's neither the "kind:key" shorthand nor a REF object) — not a live existence check against an entity row, since nothing in this codebase's meta-subject addressing does that (pkg/storage/ meta_subject.go is engine-inert by design; /meta's own handlers validate shape only, never existence). 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 that has no XOLU-LOC code reserved for it in loc-01-rest-api.md's own table (empty required fields, malformed GeoJSON, an unsupported geometry type) — a real gap in the table, not glossed over: these refusals exist and are correctly 400s, they just don't have a numbered code of their own yet.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string