0625 — one client over the datastore; resources as handles; verbs on the handle
Edit this pageContext. Five candidate shapes for initialization and usage were written out on one worked example (repo-root api-shape-0..4, folded here): 0 — all config on per-process constructors, identity at Register (shipped); 1 — identity and config on the constructor; 2 — identity at New, declaration at Register, options at the act verb; 3 — no constructors, package-level Register funcs; 4 — one client, resource structs. An independent review judged them on time-to-first-message, editor discoverability, mistake-proofing (replica config divergence, destroy without opt-in, one config governing many groups, the missing manager), cross-surface predictability, growth, and testability of user code. Ranking 4 > 2 > 1 > 3 > 0: shape 0 leaves group semantics per process, the one uncaught correctness hazard; 3 loses the destroy guard and every test seam; 1 and 2 keep admin and the manager as separate patterns and leave the missing verbs (dead-letter read, rewind, key read) no home. Storing group config closes the divergence hazard under every shape but 0, so the placement debate was ergonomics, not correctness.
Decision. vulkan.NewClient(ds, &ClientConfig{AllowDestroy, Logger, Retry}) is the one entry point; ambient config is held once and no resource config carries Logger or Retry. Two grammars: the client names the noun (RegisterConsumer[T], RegisterTopic, ListTopics, Topic(name)) because it spans nouns; a handle uses the bare verb (orders.Rename, nightly.Suspend, sys.Migrate) because it is one. Handles (vulkan.Topic, Group, Schedule, System) are a name plus the client: no I/O to acquire, Get(ctx) is the comma-ok snapshot, every other verb returns not-found itself. Every resource with children has List<Child> (adds Topic.ListGroups, Group.ListWorkers); reports (Health, Metrics, Status) keep their names. The row a user reads is <Noun>Data (TopicData, ScheduleData, SystemData, WorkerData, GroupData, MessageData[T]); datastore scan structs rename to <Table>Row to free the suffix. Every user-spelled type lives in vulkan. RegisterSchedule takes ScheduleSpec{Name, Topic, Cron}; options are nil-able pointers, nil = defaults. A declaration reports Declaration = created / joined / updated; ConsumerConfig.RequireMatch refuses a differing stored document with ErrGroupConfigMismatch. Group config is declared at RegisterConsumer and stored on the group’s worker rows; session tuning is ConsumeOptions at Consume (guides/consumer-group-config.mdx). The producer’s register-time alert pass gains “no live topic_janitor instance for this topic” as a declared Warn. Schedule.Schedule(ctx) keeps its run verb (it runs the system manager) for Register-then-run symmetry with Consume and Produce; client.RunManager stays for the produce-only deployment. The compaction key read joins the topic (Topic.CompactionHead, Topic.ListKeyMessages). Spec: website guides/client.mdx.
Rejected: Verb+Noun on handles (orders.DestroyTopic says topic twice); Info as the row suffix (JetStream’s word); fetching handles in JetStream’s js.Stream style (a cached row goes stale, which is why that API needs CachedInfo beside Info); system verbs bare on the client (a singleton today is a handle that takes no name until it does); removing the schedule run verb for the honest name; shapes 0 through 3. Amends 0619 (the Register[T] methods move to the client) and 0621 (NewScheduler becomes RegisterSchedule; Schedule(ctx) unchanged).
Amended. [0643] supersedes the clause “Every user-spelled type lives in vulkan”: every user-spelled type is aliased in vulkan from its declaring package, placed at its lowest reader — Go’s import graph forbids the package that declares the types from importing the machinery that reads them. The rest stands.
Consequences. pkg/consumer, producer, scheduler, admin, and systemmanager become the client’s assemblers behind vulkan; every lab, playground, CLI path, and site sample moves to the client. The *Data → *Row rename touches every datastore model.go and flips the CONVENTIONS ## Package layout line. New machinery: a list-groups-by-topic read, the Declaration outcome, RequireMatch and its VK code, the janitor-liveness evaluator and its VK event, vulkan.Producer[T] / Consumer[T] interfaces, a vulkantest module. Open: GetSchedule-style sugar returning a handle with its row; RequireMatch as a field versus a DeclareConsumer verb. The build is sequenced in docs/TODO.md as reviewable chunks.