M1-05: Server skeleton, module register and architecture gate¶
Status: ready | Depends on: M1-01 | Milestone: M1
Goal¶
After this ticket services/server is a buildable Go module: every module of chapter 02 exists as
a port package with its README, the cross-cutting packages live in internal/kernel, and
architecture/modules.yaml is the single source of module boundaries. A generator
(tools/archgen) turns the register into .go-arch-lint.yml, so make arch rejects every import
that breaks the layer and module rules from the first Go commit on. The kernel packages ids,
clock and errs are complete now, because every later ticket builds on them.
Read first¶
docs/PLAN.md: section "Way of working per ticket", section "M1 Foundation" (row M1-05, "Prerequisites from the core contract"), section "Decisions of this plan" (P-05, P-09, P-10, P-11, P-12).docs/spec/02-architecture.md: "Modules of the monolith" with all subsections ("Rules", "Module list", "Ports, events and sources per module", "Allowed dependencies", "Cross-cutting ports (internal/kernel)", "Actions per module", "Event types and WebSocket ops per module"), "Layers within a module", "Code layout", "Automatic check of the rules" ("Module register as the source", "go-arch-lint", "Other checks in the gate"), "Error and timeout strategy" > "Error kinds", "Open".docs/spec/03-data-model.md: "Principles" item 1 (IDs), "Modules" (the modules without tables), "Technical tables".docs/spec/06-api.md: "Basic rules" (row IDs), "Errors" including the block between<!-- BEGIN GENERATED: problems -->and<!-- END GENERATED: problems -->.docs/spec/18-release.md: "Components", "Changelog and release notes".docs/spec/23-quality.md: "Component contract", "SonarQube", "Configuration values of this chapter".- ADRs:
docs/adr/0004-*,0005-*,0008-*,0015-*,0038-*,0040-*; templatedocs/adr/0000-template-madr-4.md; ADR 0050 (exists; this ticket confirms it). docs/quality/gates.md,docs/quality/toolchain.md,CONTRIBUTING.md,services/server/README.md,services/server/internal/*/README.md,sonar-project.properties,quality/licenses/allowed.txt, the rootMakefile(targetsarch,codegen-check,lint,typecheck,test,coverage,licenses,sonaras merged by M1-01) andtools/bin/tools.lock.json(go-arch-lint pin from M1-01).
Fixed requirements that apply¶
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
- Test first: write the failing test, run it and keep its red output for the report, make it green, then clean up.
- No en dash (U+2013) or em dash (U+2014) in any file you write;
uv run --project tools --locked --quiet python tools/hooks/text_rules.py <files>over your changed files exits 0 (plaingrep -Pfails on macOS). - White label: no company, person, host or instance names, no paths of a personal machine, no real data. Examples use example.com and the people Alex, Sam and Robin.
- Configuration values only through named keys from
docs/operations/configuration.md. This ticket adds no configuration key. - Module rules 1 to 7 of 02 "Rules" and the layer table of 02 "Layers within a module" (ADR 0008): a
module is
internal/<module>, its port is the root package, other modules import only the port, the dependency graph is acyclic,kernelis usable by all,platformandapionly inadapters. - The register is the only source of module boundaries; the go-arch-lint configuration is generated from it and never edited by hand (02 "Module register as the source", R-AR-19, ADR 0050).
- IDs are UUIDv7 (RFC 9562) generated by the application; the API writes them in lower case and accepts upper case on input (03 "Principles" 1, 06 "Basic rules").
- Every error has a kind with a fixed HTTP status and a stable snake_case code; internal details never reach a response (02 "Error kinds", 06 "Errors", ADR 0015).
- The product name appears in code only at the listed places: Go module path, binary
bothy, environment prefixBOTHY_, image name (02 "Code layout", 18). go.modsets the older maintained Go line asgodirective and the built version astoolchain(ADR 0005). Module pathgithub.com/OWNER/bothy/services/serveruntil V1 (P-05).- A gap in the core contract (00, 03, 04, 05) ends the ticket with
Verdict: BLOCKED(P-11). This ticket needs 03 "Modules" to name the modules without tables (gateway,search;statsownsstats_minutessince the addendum of 2026-10-10) and the tables ofterminal(hosts,host_session_users,terminal_sessions); verify both before you start.
Scope¶
Create (all paths below services/server/ unless they start with docs/):
go.mod,go.sum;VERSIONwith0.0.0;CHANGELOG.md(Keep a Changelog, section[Unreleased])..golangci.yml(golangci-lint v2 format, module-local as 23 "Component contract" requires).architecture/modules.yaml(register, format below)..go-arch-lint.yml(generated, never edited).tools/archgen/:main.go,register.go,generate.go, tests,testdata/.cmd/bothy/:main.go,commands.go, tests.internal/kernel/ids,internal/kernel/clock,internal/kernel/errs: complete, with tests.internal/kernel/{actor,authorize,txn,eventlog,auditing,idempotency,personaldata}/doc.go.internal/wire/doc.go.internal/<module>/doc.gofor each module of 02 "Module list" (every row of that table;terminalincluded although M2 builds it, P-24).
Change:
internal/<module>/README.md(one per row of 02 "Module list"): status line and a section "Code" (see Design notes).services/server/README.md: status, layout incl.tools/archgen, health line per ruling L2 (/healthz,/readyzonserver.internal_listen_addr).docs/adr/0050-module-register-as-the-source-for-architecture-gate-and-table-ownership.md(exists): add the tests of this ticket to "Confirmation" by name;docs/adr/README.mdonly throughmake adr-index.docs/spec/02-architecture.mdonly if the build proves a statement wrong: "Code layout", "Module register as the source", "go-arch-lint" and "Error kinds" already describe what this ticket builds (module path, per-package platform components, closed public codes with internal causes).sonar-project.properties.- Root
Makefile: only the Go branch of thetestrecipe (exportGO_ARCH_LINT), and thecodegen-checkrecipe only if the rule in Design notes requires it. docs/DECISIONS.md.
Out of scope¶
- Register checked against 02, 03,
actions.yaml, the component diagram and the READMEs: M1-06. internal/platform/*packages, configuration loading and theconfigcommand: M1-07 and later.bothy serve, listeners, health: M1-11. Migrations: M1-12. Generatedapi/: M1-17.- Content of the kernel ports:
txn(M1-13),eventlog(M1-16),authorize(M1-18, M1-25),idempotency(M1-20),auditing(M1-24),actor(M1-26, M1-33),personaldata(M7). - Any use case, adapter, table or migration of a module. Gate wiring in the Makefile beyond the two recipe lines named in Scope (M1-01 owns it). CodeQL (M1-74).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M1-05-server-skeleton. - Conventional Commits with DCO sign-off (
git commit -s), scopeserver,adrorspec. - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside Scope, except docs and changelog entries named here.
- Follow the host rules supplied by the launcher. Stop only processes you started, by PID.
- Temporary directories only via
mktemp -d(in Go testst.TempDir()); remove them. - Never read or print secrets.
- No network calls except package downloads for pinned tools and Go modules, and what the
repository gates in
make checkdo themselves. Run Go withGOTOOLCHAIN=local.
Design notes¶
go.mod¶
module github.com/OWNER/bothy/services/server(P-05).go 1.26.0: the older of the two maintained Go lines on 2026-10-10 (ADR 0005).toolchainis the exact version thatgo versionprints on the host, at leastgo1.27.2.- Only dependency:
go.yaml.in/yaml/v3(the maintained continuation ofgopkg.in/yaml.v3; MIT and Apache-2.0), used bytools/archgenwithDecoder.KnownFields(true). Pin the newest v3 release. The kernel packages use the standard library only.
Register architecture/modules.yaml (normative format; later tickets extend it)¶
# SPDX-License-Identifier: Apache-2.0
# Module register: docs/spec/02-architecture.md "Module register as the source", ADR 0050.
version: 1
exclude: [tools] # directories go-arch-lint ignores (repository tools, not product code)
kernel:
core: [ids, clock, errs] # usable by every package
ports: [actor, authorize, txn, eventlog, auditing, idempotency, personaldata]
platform: {} # <name>: {depends_on: [<platform names>], vendors: [<vendor groups>]}
wire: {vendors: []}
cmd: {vendors: []}
# api: {vendors: [...]} # added by M1-17 together with api/
testsupport: {paths: [test, perf]} # test code across modules (02 "Code layout"); nobody may use it
modules:
instance:
tables: [instance, instance_settings, server_signing_keys, feature_flags, sequence_counters]
actions: [instance.discovery.read, instance.read, ...]
events: [instance.updated]
ephemeral_events: []
ws_ops: []
depends_on: []
adapter_vendors: []
# ... every module of 02 "Module list"
vendors: {} # <group>: [<import path>, <import path>/**]
- Modules appear in the order of the line "Tiers (from the bottom)" in 02 "Allowed dependencies":
instance, billing, audit, authz, identity, chat, agents, files, messaging, status, terminal, runs,
search, importer, privacy, sync, stats, notify, apps, gateway.
terminalcarries its tableshosts,host_session_usersandterminal_sessionsand its lists from 02 like every other module; M2 builds its code (P-24), this ticket only createsdoc.goand the register entry. tablesfrom 02 "Module list",actionsfrom 02 "Actions per module",events,ephemeral_eventsandws_opsfrom 02 "Event types and WebSocket ops per module",depends_onfrom 02 "Allowed dependencies", each list in the order of the chapter. Copy them exactly; M1-06 checks them against 02, 03 anddocs/spec/actions.yaml.adapter_vendors,platformandvendorsstart empty. The ticket that adds the first adapter or platform package adds its entry and vendor group (for example M1-07 addsplatform.configwith vendor groupyaml, M1-12 addspgx). A new package without a register entry failsmake arch("not attached to any component"); that is intended.- Name patterns: module and platform
^[a-z]+$, vendor group^[a-z0-9]+$. tools/archgenvalidates and exits 1 with file and YAML path on: unknown field, wrongversion, duplicate module,depends_onnaming an unknown module or itself, a dependency cycle (message lists the cycle, for examplechat -> agents -> chat), a table, action, event type or op in two modules, an undefined vendor group, a platformdepends_onthat is unknown or cyclic, empty or overlapping kernel lists.
Generator tools/archgen¶
go run ./tools/archgen -register architecture/modules.yaml -out .go-arch-lint.yml(paths relative toservices/server; these are the flag defaults). Output is byte-for-byte deterministic and starts with# SPDX-License-Identifier: Apache-2.0and# Code generated by tools/archgen from architecture/modules.yaml. DO NOT EDIT.tools/archgen/main.gocarries//go:generate go run . -register ../../architecture/modules.yaml -out ../../.go-arch-lint.yml.make codegen-check(M1-01) must catch a stale.go-arch-lint.yml. Read its recipe: if it runsgo generate ./...in every Go module and thengit diff --exit-code, the directive is enough. Otherwise add exactly one line that runs this generator before the diff, and change nothing else.- Generated content (go-arch-lint config version 3, the shape of 02 "go-arch-lint" with one
component per platform package, because M1-14 must restrict River to
platform/jobs):version: 3,workdir: .,allow: {depOnAnyVendor: false},exclude:from the register,excludeFiles: ["^.*_test\\.go$"].vendors: one entryv-<group>: {in: [...]}per register vendor group.components:kernel-core(exact pathsinternal/kernel/<core>),kernel-ports(exact pathsinternal/kernel/<port>),platform-<p>(internal/platform/<p>and.../**) per platform package,wire(internal/wire,internal/wire/**),cmd(cmd/**),api(api/**) only if the register hasapi, and per module<m>(internal/<m>),<m>-domain(internal/<m>/domain,.../**),<m>-app(internal/<m>/app,.../**),<m>-adapters(internal/<m>/adapters/**).commonComponents: [kernel-core].deps:<m>: {mayDependOn: [kernel-ports]};<m>-app: {mayDependOn: [<m>, <m>-domain, kernel-ports, <dependency ports>]};<m>-adapters: {mayDependOn: [<m>, <m>-app, <m>-domain, kernel-ports, every platform-*, api if declared, <dependency ports>], canUse: [v-<adapter vendors>]};platform-<p>: {mayDependOn: [kernel-ports, platform-<depends_on>], canUse: [...]};wire: {mayDependOn: [all module components, every platform-*, kernel-ports, api if declared], canUse: [...]};cmd: {mayDependOn: [wire, every platform-*, kernel-ports], canUse: [...]}.<m>-domainandkernel-portsget nodepsentry: go-arch-lint rejects an emptymayDependOn, and without an entry a component may use only the common component. Omit emptycanUseand omit a whole entry whose lists are both empty.testsupport(in: [test/**, perf/**]fromtestsupport.paths):depsentry withanyProjectDeps: trueandanyVendorDeps: true(verify both keys against the pinned go-arch-lint; if they differ, use its equivalent and record it); it appears in no other component'smayDependOn. Reason: shared test harnesses and load tests use the real wire graph (M1-16 createsperf/internal/report, M1-71test/harness), and production code must never import them (02 "Code layout").
- If the pinned go-arch-lint reports an error for a component whose
inmatches no package, add adoc.go(package comment only) todomainandappof every module and record it in DECISIONS.md; never make the generator drop declared components.
internal/kernel/ids¶
type ID [16]byte;func New() ID(process-wide generator: system clock,crypto/rand);type Generator;func NewGenerator(c clock.Clock, random io.Reader) *Generator;func (g *Generator) New() ID;func Parse(s string) (ID, error);func MustParse(s string) ID(panics; for tests and constants);func (ID) String() string(lower case, 8-4-4-4-12);func (ID) IsZero() bool;func (ID) Time() time.Time(UTC, millisecond);MarshalTextandUnmarshalText;var ErrInvalid(wrapped byParseerrors).- Layout per RFC 9562 section 5.7: 48-bit Unix milliseconds, version nibble
0111, 12 bitsrand_a, variant10, 62 bitsrand_b. - Monotonic per process (RFC 9562 section 6.2, method "Monotonic Random"), guarded by a mutex: in the
same millisecond as the last ID, add a random value in
[1, 2^32)to the 74 bits ofrand_aandrand_btaken as one counter; on overflow advance the timestamp by one millisecond and draw fresh random bits; if the clock goes backwards, keep the last timestamp. A failing random reader is a programming error: panic with a message naming the package. Parseaccepts upper and lower case, requires version 7 and variant10, and rejects the nil and max UUID, braces, theurn:uuid:prefix and any other length.
internal/kernel/clock¶
type Clock interface { Now() time.Time }; func System() Clock (returns UTC);
type Fake with func NewFake(t time.Time) *Fake, Now, Set, Advance(d time.Duration); safe
for concurrent use. The fake lives in the same package so that kernel-core keeps exact paths.
internal/kernel/errs (aligns 02 "Error kinds" with 06 "Errors")¶
type Kind uint8withStatus() intandString() string(snake_case). Kinds and status:bad_request400,unauthenticated401,step_up_required401,forbidden403,not_found404,method_not_allowed405,conflict409,invariant_violation409,gone410,precondition_failed412,too_large413,unsupported_media_type415,range_not_satisfiable416,unprocessable422,precondition_required428,rate_limited429,internal500,unavailable503. The zero valueKindNonehasStatus() == 0.type Code stringwith one constant per code of the 06 block (for exampleCodeVersionConflict = "version_conflict"), andfunc AllCodes() []Code. Each code has exactly one kind; the kind follows from the code, never from the caller. Mapping:validation_failed,malformed_request,cursor_invalid,webhook_host_not_allowedtobad_request;unauthenticated,token_expired,session_revoked,use_dpop_nonce,invalid_dpop_prooftounauthenticated;step_up_requiredtostep_up_required;forbidden,scope_missing,device_pending,channel_read_only,feature_disabled,agent_invocation_denied,invocation_token_invalidtoforbidden;not_found;method_not_allowed;conflict,request_in_progress,already_started,billing_mode_mismatch,tool_class_mismatch,invalid_state_transition,base_version_conflict,component_used,operator_mismatch,file_not_ready,file_blocked,title_manualtoconflict;i1_violation,last_ownertoinvariant_violation;resync_required,message_deleted,gonetogone;version_conflicttoprecondition_failed;payload_too_largetotoo_large;unsupported_media_type;range_not_satisfiable;idempotency_key_reusetounprocessable;precondition_required;rate_limited;internal_errortointernal;unavailable,maintenance,setup_requiredtounavailable. The code list followsx-problem-codes; a code added there later gets its kind in the same change (the test overAllCodes()compares both).type Error structwith fieldsCode Code,Detail string,Reason string,Hint string,Cause string,Fields []FieldError,Current any,RetryAfter time.Durationand an unexported wrapped error;type FieldError struct{ Pointer, Code, Detail string }. ConstructorsNew(code Code, detail string) *ErrorandWrap(code Code, err error, detail string) *Error(both panic on a code without kind: programming error), settersWithReason,WithHint,WithCause,WithFields,WithCurrent,WithRetryAfterreturning the receiver;Kind() Kind,Unwrap() error;Error()returns<code>or<code>: <detail>and never the cause or the wrapped error;LogValue() slog.Value(packagelog/slog) adds kind, cause and the wrapped error for logs. Package functionsAs(err error) (*Error, bool)andKindOf(err error) Kind(KindNonefor nil,internalfor any error that is not an*Error).Causeis an internal label for logs and metrics, never serialized into a response. Constants for the internal causes 02 names:CauseAuthzNotChecked = "authz_not_checked",CauseUnknownAction = "unknown_action",CauseDBPoolExhausted = "db_pool_exhausted",CauseShuttingDown = "shutting_down",CauseDeadlineExceeded = "deadline_exceeded",CauseDependencyUnavailable = "dependency_unavailable". Reason: 06 defines the closed set of public codes, and 02 forbids internal details in responses.
Port packages, kernel ports, wire¶
internal/<m>/doc.go: SPDX line, then// Package <m> is the port of the <m> module: <its responsibility from 02 "Module list", in English>. Other modules import only this package (docs/spec/02-architecture.md, "Layers within a module").No exported symbols yet: interfaces enter with the ticket that implements them, so nothing is declared that no code uses.internal/kernel/<port>/doc.go: purpose, implementing module and users from 02 "Cross-cutting ports".internal/wire/doc.go: wiring per role and registration of in-transaction subscribers (02 "Code layout", "In-Tx events").- Module READMEs are English. Replace the status line with
Status: skeleton (M1-05). The port package exists; interfaces and use cases come with the tickets of the milestone.Add a section## Codelisting the packages that exist. Keep the lineMay use the ports of these modules: ...and the section## Tablesexactly in their current form; M1-06 parses them.
cmd/bothy¶
maincallsos.Exit(run(ctx, os.Args[1:], os.Stdout, os.Stderr)).commands.goholdstype command struct { name, summary string; run func(ctx context.Context, args []string, stdout, stderr io.Writer) int }and the tablecommands. This ticket registers no command; M1-07 addsconfig, M1-11serve, and so on (02 "Commands of the binary" is the register of names).- Exit codes: 0 success, 1 failure, 2 usage error. No arguments: usage on stderr, exit 2.
help,-h,--help: usage on stdout, exit 0. Unknown command:bothy: unknown command "<name>"plus usage on stderr, exit 2. Usage lists registered commands sorted by name. Standard libraryflagonly; no CLI framework (small surface, no dependency).
Lint and Sonar¶
.golangci.yml:version: "2";linters.default: standard; enablerevive(rulesexportedandpackage-comments: this enforces a doc comment on every exported symbol),gosec,errorlint,bodyclose,noctx,misspell,unparam,gocritic,exhaustive,errname,nolintlint(explanation and specific linter required); formattersgofmtandgoimportswith the module path as local prefix. Never enable the exclusion presetcomments. Validate withgolangci-lint config verify.sonar-project.properties: add**/testdata/**and**/sqlcgen/**tosonar.exclusions. Rule for all generated Go files in this repository: file names end in.gen.go(already excluded and exempt from SPDX headers intools/ci/license_check.py); record the rule in DECISIONS.md.
Documentation of decisions¶
02 and ADR 0050 already state the register format, the per-package platform components, the
module path (P-05) and the closed public codes. Record only the remaining choices in
docs/DECISIONS.md: own UUIDv7 implementation instead of a library (stdlib only in kernel); YAML
library choice; .gen.go rule; doc.go fallback if it was needed. Add the tests of this ticket to
the "Confirmation" of ADR 0050 and run make adr-index.
Tests (write them first)¶
internal/kernel/ids/ids_test.go:
TestNewHasVersion7AndVariantTestNewIsStrictlyMonotonicWithinOneMillisecond(fake clock fixed; 10 000 IDs strictly increasing in byte order)TestNewNeverGoesBackwardsWhenClockDoesTestNewEmbedsClockMillisecondsTestNewCounterOverflowAdvancesTimestamp(random reader of0xffbytes)TestNewConcurrentUnique(64 goroutines x 1 000 IDs, no duplicate, run with-race)TestParseRFC9562Example:017F22E2-79B0-7CC3-98C4-DC0C0C07398Fparses,String()is lower case,Time()is2022-02-22T19:22:22Z.TestParseRejects(table): v4f47ac10b-58cc-4372-a567-0e02b2c3d479, v1c232ab00-9414-11ec-b3c8-9f6bdeced846, nil,ffffffff-ffff-ffff-ffff-ffffffffffff, wrong variant017f22e2-79b0-7cc3-18c4-dc0c0c07398f, 35 and 37 characters, braces,urn:uuid:prefix, non-hex.TestTextRoundTrip
internal/kernel/clock/clock_test.go: TestSystemReturnsUTC, TestFakeSetAndAdvance,
TestFakeConcurrentUse.
internal/kernel/errs/errs_test.go:
TestKindStatus(table above).TestCodesMatchChapter06: find the repository root by walking up to the directory that containsdocs/spec, read the generated block ofdocs/spec/06-api.md, and assert that its code set equalsAllCodes()and that each code's status equalsCode.Kind().Status(). This holds the code against the real document.TestEveryCodeHasKind,TestNewPanicsOnUnknownCode.TestKindOf(nil, plain error, wrapped*Errorviafmt.Errorf("%w")).TestErrorStringHidesCauseAndWrapped,TestLogValueContainsCause.
cmd/bothy/main_test.go: TestRunWithoutArgumentsPrintsUsageAndExits2, TestRunHelpExits0,
TestRunUnknownCommandExits2.
tools/archgen/*_test.go:
TestGenerateGolden(testdata/golden/modules.yamltotestdata/golden/go-arch-lint.yml),TestGenerateIsDeterministic.TestRegisterRejects(table, one case per validation rule above).TestRepositoryRegisterIsValidandTestRepositoryConfigIsCurrent(generating from../../architecture/modules.yamlequals../../.go-arch-lint.ymlbyte for byte).TestRepositoryRegisterCoversModuleList: read the realdocs/spec/02-architecture.md, collect the module names of the table "Module list" and of the line "Tiers (from the bottom)", and assert that the register has exactly these modules in the order of that line. No module count is hard-coded; a new row in 02 fails this test until the register follows. M1-06 checks the lists inside each entry.TestArchLintSkeletonClean,TestArchLintDomainImportsDriver,TestArchLintAppImportsForeignPort,TestArchLintAppImportsForeignApp,TestArchLintPackageWithoutComponent: copytestdata/skeleton(its owngo.modwith moduleexample.com/skeleton, a register with moduleschat,messaging,runs, and a local stub module forgithub.com/jackc/pgx/v5wired by areplacedirective tothird_party/pgx, listed underexclude) intot.TempDir(), add the one violating file of the case, generate the config, run go-arch-lint withGOFLAGS=-mod=mod,GOPROXY=off,GOTOOLCHAIN=local,GOWORK=off, and assert exit code 0 for the clean case and exit code 1 plus the offending package path in the output for each violation (the four kinds of 02 "go-arch-lint"). Fixture.gofiles carry the SPDX line and are gofmt-clean.TestArchLintTestSupport: in the skeleton, a package undertest/importing a module adapter passes; a module adapter importing a package undertest/fails with exit code 1.- The tests find go-arch-lint through the environment variable
GO_ARCH_LINT. If it is unset they fail withset GO_ARCH_LINT to the pinned go-arch-lint (make test does this); they never skip (R-QA-02). In the rootMakefile, the Go branch oftestsetsGO_ARCH_LINT=$(call tool,go-arch-lint)forgo test.
Commands that must pass, in this order:
cd services/server && GOTOOLCHAIN=local go build ./... && go vet ./...
cd services/server && GO_ARCH_LINT="$(uv run --project ../../tools --locked --quiet python ../../tools/bin/fetch_tools.py path go-arch-lint)" go test -race ./...
make arch codegen-check fmt-check lint typecheck test coverage licenses osv semgrep docs linkcheck
make sonar
make check
Acceptance criteria¶
-
make archexits 0 on the skeleton (go-arch-lint "No warnings found"). -
make codegen-checkpasses; changingmodules.yamlwithout regenerating makes it fail (show this once in the report). - The four violation fixtures fail with exit code 1 and name the offending package; the clean fixture passes.
-
TestRegisterRejectscovers every validation rule listed in Design notes. - The register lists every module of 02 "Module list" (
TestRepositoryRegisterCoversModuleList;terminalincluded) with tables, actions, events, ephemeral events, ops and dependencies copied from 02. -
kernel/ids,kernel/clockandkernel/errstests pass with-race;TestCodesMatchChapter06reads the realdocs/spec/06-api.md. - Every exported symbol has a doc comment (revive
exportedclean); every package has a package comment. - Go coverage of the module is at least
quality.coverage.go_min_percent;make sonarpasses the gate "Bothy way". - 02 still matches the built register and generator; ADR 0050 names this ticket's tests and the ADR index is regenerated.
-
make checkis green.
Definition of done¶
- Code, tests and docs as above: module READMEs updated, doc comments on every public symbol, ADR
0050 "Confirmation", DECISIONS.md entries,
services/server/CHANGELOG.mdentry under[Unreleased](Added). make checkgreen.- Independent review over the diff (a fresh reviewer,
make review) and the red team gate ofdocs/quality/red-team-gate.mdover the diff, verdict recorded in the report; fix every finding with a test and have the delta checked again. - Merge locally into the base branch only if review and red team are green and
make checkpasses on the merged state; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}: first line Verdict: DONE or Verdict: BLOCKED <reason>; then commits
(SHA and subject), files with line counts, test results with counts (including the red output of
the first test run), what is not proven, and follow-ups that could not be done in scope with the
reason. Then write {{DONE_PATH}} with exit=0 sha=<merge sha> or exit=1 <reason>.