Skip to content

Teach the AI_LM seam which yard an order ships from - #1

Merged
futurebuildai merged 1 commit into
mainfrom
ai-lm/branch-depot
Aug 31, 2026
Merged

Teach the AI_LM seam which yard an order ships from#1
futurebuildai merged 1 commit into
mainfrom
ai-lm/branch-depot

Conversation

@futurebuildai

Copy link
Copy Markdown
Contributor

The ERP half of the branch-depot change. Merge this before
gable-ai-lm#10
, which consumes
the endpoint added here.

Why

A dealer with more than one yard ships from several branches on the same day. AI_LM planned
the whole run from one depot (DEPOT_LAT/DEPOT_LNG), so some routes were silently rooted at
the wrong yard. Gable already knows the answer per order — orders.branch_id has been
NOT NULL since migration 062, and locations.latitude/longitude have existed since 072,
lazily backfilled by the same OpenRouteService geocoding the delivery service already uses for
resolveBranchOrigin. None of it was reachable across the integration seam.

What changed

branch_id on the integration Order DTO — a plain string, third field, not a pointer
and not omitempty. orders.branch_id is NOT NULL, so an empty string on the wire is an
ERP bug the consumer should be able to observe rather than something that quietly reads as
"no branch."

New GET /api/integration/locations — the dealer's branches: id, name, address,
latitude, longitude. Same X-Integration-Key auth as its six siblings (unconfigured → 503,
missing/wrong key → 401), bare JSON array, never null.

Two details that are load-bearing for the consumer:

  • Coordinates are nullable and not COALESCEd. A branch that has never been geocoded omits
    the keys entirely. nil means "never geocoded — do not root a route here"; it is not 0,0.
    AI_LM's fallback logic depends on being able to tell those apart.
  • address is composed by the same rule as delivery.composeBranchAddress, so a branch
    geocoded via either path resolves to the same point. name falls back to locations.code
    when the name is null/empty, so it is never blank.

Rows returned are type='BRANCH' AND parent_id IS NULL AND active = TRUE. An order whose
branch_id has no match in that list is "branch unknown" to the consumer, which falls back
rather than failing.

No migration. 062 and 072 already provide everything.

Contract suite

ailm_contract_test.go and its goldens are the only thing keeping the two repos in step, so
they are updated in the same change: the endpoint inventory now pins seven routes, the
Order fixture carries branch_id, and ailm_locations.json is added. Goldens were
regenerated with -update-golden and the diff reviewed — only the three intended changes moved.

One deviation worth flagging: the mirror structs in that file are normally copied verbatim
from
AI_LM. Here they were written ahead of it, because neither repo can merge half a seam.
The mirror header now documents that as an explicit, narrow exception. Adversarial review
confirmed the two sides match field-for-field, tag-for-tag, order-for-order.

Known gaps (pre-existing, not introduced here)

  • The goldens are fake-store driven, so the actual SQL — the type='BRANCH' predicate, the
    COALESCE(NULLIF(name,''), code) fallback, branch_id::text — is executed by no test. There
    is no DB-backed test infrastructure in this repo at all. Column names were hand-verified
    against the migrations.
  • composeBranchAddress is now duplicated between internal/integrations and
    internal/delivery, byte-identical but bound only by a comment. A future edit to one drifts
    silently.

Gates

gofmt · go vet · go build · go test -race ./...50 packages ok, including the
updated contract suite. No migration, no new dependency, no change to cmd/server/main.go
(the handler block was already registered and /api/integration/ is already on PublicPaths,
so only X-Integration-Key gates it — verified by the inventory test).

AI_LM plans an entire day from ONE depot: DEPOT_LAT/DEPOT_LNG, a single
global config pair. That is fine for a one-yard dealer and quietly wrong
for everyone else. A dealer with two or three branches ships from all of
them on the same morning, so every route whose orders leave from a yard
other than the configured one is optimized from the wrong origin — the
first and last legs are fiction, the drive-time estimate is wrong, and
nothing anywhere says so. This ERP already knows the right answer per
order and simply never told AI_LM.

Phase 1 is the data half of the fix, on this side of the seam only:

  * orders.branch_id (NOT NULL since migration 062) now reaches the wire
    as `branch_id` on the integration order DTO. Deliberately NOT
    omitempty — an order always has a branch, so an empty value is an ERP
    bug and AI_LM must be able to see it rather than receive a payload
    where "no branch" and "field dropped" are indistinguishable.

  * New GET /api/integration/locations returns the dealer's active
    branches (id, name, composed address, latitude, longitude), behind
    the same X-Integration-Key middleware as its six siblings, as a bare
    JSON array like every other list endpoint.

    latitude/longitude are *float64 and are NOT COALESCEd in the SQL.
    locations.latitude/longitude are backfilled lazily (migration 072,
    geocoded the first time a route is optimized from that branch), so
    NULL is a real and common state. Collapsing it to 0 would hand AI_LM
    a yard at null island — in the Gulf of Guinea — that it cannot tell
    apart from a genuine coordinate, which is a worse failure than the
    one being fixed. Nil lets AI_LM say "this yard has never been
    geocoded, falling back" on the plan.

    composeBranchAddress is duplicated from internal/delivery rather than
    imported: this package must stay a leaf that httptest can drive
    without Postgres. The two must agree, or a branch geocoded through
    one path lands somewhere else than the same branch geocoded through
    the other, so the rule is unit-tested here.

No migration: 062 and 072 already provide every column this needs.

The cross-repo conformance suite is updated in the same commit, which is
the point of it existing — the endpoint inventory fails on an
unregistered route and Layer A pins the mirror of AI_LM's client structs.
aiLMLocation and aiLMOrder.BranchID are written into the mirror as the
AGREED shape ahead of AI_LM's client.go, because neither repo can merge
half a seam; the mirror header now records that exception explicitly so a
future reader does not mistake it for someone "fixing" the mirror to make
a GableLBM change pass. AI_LM's gable.Location and gable.Order must match
field for field, tag for tag, in the same order.

Consuming this — the BRANCH depot source that outranks config, and the
honest fallback when a day's orders span multiple branches — is Phase 2,
in the AI_LM repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VS2YVQehtn78L2Cq49TSj3
@futurebuildai
futurebuildai merged commit 8361f23 into main Aug 31, 2026
5 checks passed
@futurebuildai
futurebuildai deleted the ai-lm/branch-depot branch August 31, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant