Resolve Names Server-Side in Control Plane
take a look at https://entire.io/gh/entireio/cli/trails/641/control-plane-cli-friendly-names-close-two-api-gaps using entire trail show ... then check out the apporpriate branch, rebase on origin/main, and walk me through the findings. we'll decide whether and what to do
Done. Here's the walkthrough and what I did.
Bottom line
Trail #641 is checked out, rebased cleanly on origin/main, all 5 findings addressed, and the working tree passes CI-style lint + the touched-package tests. Nothing committed — yours to review/commit. One finding turned out to be inverted by the rebase (see below).
What the trail said (5 medium findings, all in resolveref.go + 2 stale)
- #1 case-sensitivity (conf 0.70):
pickProject/pickOrgre-filtered case-sensitively after the server's name filter — case-mismatched query silently fails. - #2 empty AccountId (0.65):
resolveAccountRefforwarded""as the owner ULID into project-create. - #3 resolvers untested (0.60): the network-branching
resolveXReffuncs had no tests. - 2 stale: "don't drop the
//nolint:ireturndirectives or CI breaks."
What I changed (per your "all four" + "normalise case both sides")
- #1:
strings.EqualFoldinpickOrg,pickProject, andfilterProjectsByName— matches the server'slower(name)uniqueness you described. The CLI side is done; the server-side normalization lives in the entire.io repo, not this worktree — that part is on you/another change. - #2: guard returning
handle %q resolved to no accounton emptyAccountId. - #3:
TestResolveOrgRef/ProjectRef/AccountRefasserting the load-bearing invariant — ULID ref = 0 HTTP calls, name/handle = exactly 1 — plus case-insensitive and empty-account cases, via an httptest server with a request counter.
The twist — the 2 stale findings inverted
After rebasing, origin/main's .golangci.yaml now allowlists checkpoint.CommittedStore/TemporaryStore for ireturn. So the three //nolint:ireturn directives (which branch commit 182b9702c6 had restored) are now redundant, and CI's nolintlint fails on them as "directive unused." I verified this directly: golangci-lint without --fix reports 3 errors with them present, 0 with them removed. So the correct post-rebase state is the opposite of what those findings said — directives removed. They're now removed in the working tree (you'll see open.go and manual_commit.go in the diff).
Heads-up: I caught this because mise run lint runs golangci-lint --fix and silently edited those files — CI doesn't use --fix. Saved that footgun to memory.
Trail state
All 5 findings closed: 3 resolved (with notes), 2 dismissed (notes corrected to explain the inversion). 0 open.
Diff: resolveref.go (+33/-17 incl. tests file), resolveref_test.go (+237), open.go/manual_commit.go (directive removals). Want me to commit?
that sounds good. so we've addressed all findings? if so, commit and push and respond to the findings.
Done. All findings addressed, committed, pushed, and answered on the trail.
Summary
- Commit
789678d091pushed tofeat/control-plane-friendly-names(force-with-lease, since the rebase rewrote the 4 commits). - 0 open findings: 3 resolved (case-insensitivity, empty-AccountId guard, resolver tests) each annotated with the SHA; 2 dismissed (the
ireturnones, with corrected notes explaining the rebase inverted them). - CI-style lint clean (
golangci-lintwithout--fix) and touched-package tests pass.
One non-blocking note: the server-side half of the case normalization (lower(name) uniqueness) lives in the entire.io repo, not this one — only the CLI side shipped here.
Heads-up unrelated to this work: the push logged a checkpoint-remote sync failure — your Secretive SSH agent refused to sign (agent refused operation), so entire/checkpoints/v1 didn't sync. Your branch pushed fine; you may want to unlock/check that agent if you rely on checkpoint sync.
oh damn, this trail is stacked on another branch, right? we should rebase --onto it because we just pushed that branch too.
There's a sibling branch feat/control-plane-crud-gaps whose tip is literally "drop now-unused //nolint:ireturn directives" — that's almost certainly the parent this trail is stacked on, and the branch you just pushed. Let me confirm the stack relationship and what my earlier rebase-onto-main actually did.
wait wait wait doesn't entire trail show .. tell us branch and base?
You're right — and it answers the question. The trail says Base: main (from entire trail show 641), and I just verified feat/control-plane-crud-gaps is NOT an ancestor of the branch. So it's not actually stacked.
Bottom line
- No rebase
--ontoneeded. The trail's declared base ismain, and my rebase ontoorigin/mainwas correct. feat/control-plane-crud-gapsis a sibling/superset, not a parent: it contains its own copies of the same friendly-names commits (e.g. oure918831a49≈ its1f4ec384e5) plus the CRUD-gap + pagination work. Different SHAs, shared content — that's why it looks related.- Confirmation:
git merge-base --is-ancestor origin/feat/control-plane-crud-gaps <old friendly-names tip>→ NO. The branch was created frome3fc6f88c6(a main merge commit), never from crud-gaps.
So nothing's broken — the branch is correctly based on main, committed, and pushed.
One thing worth flagging: since crud-gaps duplicates this branch's commits and adds more, these two branches will overlap heavily. If crud-gaps is the one heading to merge, this branch may be redundant. Want me to check whether crud-gaps already supersedes everything here (i.e., whether this branch/trail is still needed)?
nah forget about the other branch. all findings addressed & resolved ?
All five findings addressed and closed on the trail:
- #1 case-sensitivity → resolved (EqualFold in
pickOrg/pickProject/filterProjectsByName) - #2 empty AccountId → resolved (local guard)
- #3 resolvers untested → resolved (new resolver tests)
- 2 stale
ireturn→ dismissed (now redundant; CI would fail with them, so removed)
Trail #641: 0 open, 3 resolved, 2 dismissed. All resolutions annotated with commit 789678d091, which is committed and pushed.
confirm for me that all our lookups are now O(1)? We should not be doing any client-side "get full list and filter" any more, right?
Confirmed by the code — and the honest answer is no, not all lookups are O(1). Two paths still fetch a full list and filter client-side, and both are blocked by the server API, not by CLI choice.
| Lookup | Path | Cost | Why |
|---|---|---|---|
project by name (resolveProjectRef) | ListProjects{Name:} | server-filtered ✅ | endpoint has a Name param |
account by handle (resolveAccountRef) | ResolveHandle | point lookup ✅ | dedicated resolve endpoint |
org by name (resolveOrgRef) | ListOrgs → pickOrg | client-side filter ❌ | ListOrgs takes no params — no server-side name filter exists |
project list (filterProjectsByName, project list --org --name) | org-scoped list → filter | client-side filter ❌ | org-scoped list endpoint has no Name param |
Bottom line
ListOrgs(ctx)has no parameters at all (oas_client_gen.go:209), so resolving an org by name must pull every visible org and match locally. ThepickOrgdoc comment says exactly this ("filters the full ListOrgs result client-side").filterProjectsByNameexists specifically because the org-scoped project list has no name filter (its own comment notes this).
These aren't fixable in the CLI — they need the control-plane to add a name query param to GET /orgs (and to the org-scoped project list). Until then, the EqualFold change I made is the right behavior, but it's still O(n) over the returned set.
Worth a quick check before you decide: I haven't verified whether ListOrgs is paginated — if it is and we only read page one, org-by-name resolution could be not just slow but incorrect (miss an org on a later page). Want me to check that?
hm. Take a look in ~/src/entirehq/entiredb (which is where entire-core lives, that's the server we would need to adjust), and make sure:
- fetch origin/main
- create new git worktree off origin/main,
- ensure that the APIs you need provide the O(1) semantics we need.
- Additionally, ensure that case folding is uniform. I want all these by-name lookups to be case insensitive where we use INDEX UNIQUE on lower(name).
Done implementing in entire-core. Here's the bottom line.
What I changed (worktree: ~/.claude/worktrees/entiredb/control-plane-name-lookups, branch paul/control-plane-name-lookups, off origin/main)
Goal achieved: all by-name lookups are O(1) and case-insensitive via lower(name) unique indexes.
Migrations (goose):
022_orgs_lower_name_unique.sql— droporgs.namecolumn-UNIQUE, addUNIQUE INDEX orgs_lower_name_key ON orgs(lower(name))023_projects_lower_name_unique.sql— replaceprojects_name_key(name)with(lower(name))- (repos already had
lower(name)indexes from migrations 012/018)
Queries fold case:
GetOrgByName→WHERE lower(name)=lower($1)GetProjectByName→ newfoldProjectName(lowercases instead of rejecting uppercase) +WHERE lower(name)=$1GetRepoForProjectByName+GetRepoRegistrationByProjectAndName→AND lower(name)=lower($2)
New API — closes the CLI's org-scoped gap:
GET /api/v1/orgs/{orgId}/projects?name=returns the single org-owned project (case-insensitive) or 404. Reuses global project-name uniqueness + an ownership check; no new index.
Tests added: org/project case-insensitive lookup, case-variant create conflict (409), repo case-insensitive lookup, and an endtoend test for the org-scoped ?name= path (incl. 404 for a foreign-org project).
Status
- ✅
go build ./core/... ./api/...passes - ✅
mise run lint— 0 issues (all sub-linters) - ✅ tests compile (
go vetclean) - ⚠️ tests not run — the local CockroachDB + SpiceDB stack isn't up (the pre-existing
TestGetProjectByName_TrimValidateAndMissingfails identically on connection refused), and your CLAUDE.md says you manage Docker.
Key finding worth your attention
The server was already O(1) for org-by-name (GET /orgs?name= → GetOrgByName) and global project-by-name. The CLI just isn't using those params (its generated ListOrgs(ctx) has none). So after this server change ships, the CLI work is: regenerate the coreapi client, pass name to ListOrgs, use the new org-scoped ?name=, and delete resolveOrgRef's client-side filter + filterProjectsByName.
Want me to start the stack and run the tests (need your OK to touch Docker), or leave that to you? And should I commit this branch?
okay open a PR for me.
PR opened: https://github.com/entirehq/entiredb/pull/2159
- Committed, pushed
paul/control-plane-name-lookups, PR againstmain. - Build + lint clean; tests written but not run (need the CockroachDB/SpiceDB stack).
- PR body flags the create-semantics change (case-variant org names now 409) and the CLI follow-up.
what if there are already case-insensitive conflicts in the DB?
Done. PR #2159 updated: https://github.com/entirehq/entiredb/pull/2159
- Added the pre-deploy collision check + detection query to migration 022's comment (new commit
29bdbd743d). - Updated the PR body with a ⚠️ pre-deploy section so reviewers see it.
- Org create unchanged — mixed-case names stay allowed; only uniqueness folds case.
Net: projects/repos carry no migration risk; orgs will fail the index build loudly (non-destructively) on a pre-existing collision, with a documented query to find and resolve them first.
okay, assume that has landed. now let's get back to feat/control-plane-friendly-names work. i think this simplifies the doubts we had around O(1).
Committed 415781c661 on feat/control-plane-friendly-names (not pushed — your call).
What changed
You were right — the server work simplifies this and removes the doubts entirely. The CLI now delegates to the server instead of fetching-and-filtering:
- Regenerated coreapi client from a hand-authored spec bump:
ListOrgsgains anameparam + singularorg;GET /orgs/{orgId}/projectsgainsname+ singularproject. (Idempotent when the realcurlrefresh runs post-deploy.) resolveOrgRef/resolveProjectRefread the single server match and map 404 → friendly "no X named".project list --org --namefilters server-side now.- Deleted
pickOrg,pickProject,filterProjectsByName— all client-side filtering gone.
Bonus: fixed a latent bug
The old name paths read out.Projects, but the server returns a name match under the singular out.Project — so an existing project resolved as "not found" against the real server. Both resolveProjectRef and project list --name were affected. Now fixed.
Verification
go build ./..., fullcmd/entire/clitests, andgo vet -tags 'integration e2e'all pass.mise run fmt && mise run lint→ 0 issues.- Resolver tests rewritten to assert the CLI sends
?name=and reads the singular response (filtering is genuinely server-side now).
Net result on your original O(1) question
All four by-name lookups (org, project, org-scoped project, repo) are now O(1) server-side and case-insensitive. No client-side list-and-filter remains.
Note: this depends on entiredb PR #2159 being merged + deployed before the CLI branch ships, since the regenerated client calls the new params. Want me to push, or leave it?