Skip to content

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-*; template docs/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 root Makefile (targets arch, codegen-check, lint, typecheck, test, coverage, licenses, sonar as merged by M1-01) and tools/bin/tools.lock.json (go-arch-lint pin from M1-01).

Fixed requirements that apply

  1. No MVP. New from scratch and directly in the final state; stages are only the order.
  2. Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
  3. 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).
  4. 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.
  5. Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
  6. Test first: write the failing test, run it and keep its red output for the report, make it green, then clean up.
  7. 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 (plain grep -P fails on macOS).
  8. 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.
  9. Configuration values only through named keys from docs/operations/configuration.md. This ticket adds no configuration key.
  10. 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, kernel is usable by all, platform and api only in adapters.
  11. 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).
  12. 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").
  13. 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).
  14. The product name appears in code only at the listed places: Go module path, binary bothy, environment prefix BOTHY_, image name (02 "Code layout", 18).
  15. go.mod sets the older maintained Go line as go directive and the built version as toolchain (ADR 0005). Module path github.com/OWNER/bothy/services/server until V1 (P-05).
  16. 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; stats owns stats_minutes since the addendum of 2026-10-10) and the tables of terminal (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; VERSION with 0.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.go for each module of 02 "Module list" (every row of that table; terminal included 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, /readyz on server.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.md only through make adr-index.
  • docs/spec/02-architecture.md only 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 the test recipe (export GO_ARCH_LINT), and the codegen-check recipe 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 the config command: M1-07 and later.
  • bothy serve, listeners, health: M1-11. Migrations: M1-12. Generated api/: 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), scope server, adr or spec.
  • 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 tests t.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 check do themselves. Run Go with GOTOOLCHAIN=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). toolchain is the exact version that go version prints on the host, at least go1.27.2.
  • Only dependency: go.yaml.in/yaml/v3 (the maintained continuation of gopkg.in/yaml.v3; MIT and Apache-2.0), used by tools/archgen with Decoder.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. terminal carries its tables hosts, host_session_users and terminal_sessions and its lists from 02 like every other module; M2 builds its code (P-24), this ticket only creates doc.go and the register entry.
  • tables from 02 "Module list", actions from 02 "Actions per module", events, ephemeral_events and ws_ops from 02 "Event types and WebSocket ops per module", depends_on from 02 "Allowed dependencies", each list in the order of the chapter. Copy them exactly; M1-06 checks them against 02, 03 and docs/spec/actions.yaml.
  • adapter_vendors, platform and vendors start empty. The ticket that adds the first adapter or platform package adds its entry and vendor group (for example M1-07 adds platform.config with vendor group yaml, M1-12 adds pgx). A new package without a register entry fails make arch ("not attached to any component"); that is intended.
  • Name patterns: module and platform ^[a-z]+$, vendor group ^[a-z0-9]+$.
  • tools/archgen validates and exits 1 with file and YAML path on: unknown field, wrong version, duplicate module, depends_on naming an unknown module or itself, a dependency cycle (message lists the cycle, for example chat -> agents -> chat), a table, action, event type or op in two modules, an undefined vendor group, a platform depends_on that 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 to services/server; these are the flag defaults). Output is byte-for-byte deterministic and starts with # SPDX-License-Identifier: Apache-2.0 and # Code generated by tools/archgen from architecture/modules.yaml. DO NOT EDIT.
  • tools/archgen/main.go carries //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 runs go generate ./... in every Go module and then git 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 entry v-<group>: {in: [...]} per register vendor group.
    • components: kernel-core (exact paths internal/kernel/<core>), kernel-ports (exact paths internal/kernel/<port>), platform-<p> (internal/platform/<p> and .../**) per platform package, wire (internal/wire, internal/wire/**), cmd (cmd/**), api (api/**) only if the register has api, 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>-domain and kernel-ports get no deps entry: go-arch-lint rejects an empty mayDependOn, and without an entry a component may use only the common component. Omit empty canUse and omit a whole entry whose lists are both empty.
    • testsupport (in: [test/**, perf/**] from testsupport.paths): deps entry with anyProjectDeps: true and anyVendorDeps: 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's mayDependOn. Reason: shared test harnesses and load tests use the real wire graph (M1-16 creates perf/internal/report, M1-71 test/harness), and production code must never import them (02 "Code layout").
  • If the pinned go-arch-lint reports an error for a component whose in matches no package, add a doc.go (package comment only) to domain and app of 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); MarshalText and UnmarshalText; var ErrInvalid (wrapped by Parse errors).
  • Layout per RFC 9562 section 5.7: 48-bit Unix milliseconds, version nibble 0111, 12 bits rand_a, variant 10, 62 bits rand_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 of rand_a and rand_b taken 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.
  • Parse accepts upper and lower case, requires version 7 and variant 10, and rejects the nil and max UUID, braces, the urn: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 uint8 with Status() int and String() string (snake_case). Kinds and status: bad_request 400, unauthenticated 401, step_up_required 401, forbidden 403, not_found 404, method_not_allowed 405, conflict 409, invariant_violation 409, gone 410, precondition_failed 412, too_large 413, unsupported_media_type 415, range_not_satisfiable 416, unprocessable 422, precondition_required 428, rate_limited 429, internal 500, unavailable 503. The zero value KindNone has Status() == 0.
  • type Code string with one constant per code of the 06 block (for example CodeVersionConflict = "version_conflict"), and func 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_allowed to bad_request; unauthenticated, token_expired, session_revoked, use_dpop_nonce, invalid_dpop_proof to unauthenticated; step_up_required to step_up_required; forbidden, scope_missing, device_pending, channel_read_only, feature_disabled, agent_invocation_denied, invocation_token_invalid to forbidden; 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_manual to conflict; i1_violation, last_owner to invariant_violation; resync_required, message_deleted, gone to gone; version_conflict to precondition_failed; payload_too_large to too_large; unsupported_media_type; range_not_satisfiable; idempotency_key_reuse to unprocessable; precondition_required; rate_limited; internal_error to internal; unavailable, maintenance, setup_required to unavailable. The code list follows x-problem-codes; a code added there later gets its kind in the same change (the test over AllCodes() compares both).
  • type Error struct with fields Code Code, Detail string, Reason string, Hint string, Cause string, Fields []FieldError, Current any, RetryAfter time.Duration and an unexported wrapped error; type FieldError struct{ Pointer, Code, Detail string }. Constructors New(code Code, detail string) *Error and Wrap(code Code, err error, detail string) *Error (both panic on a code without kind: programming error), setters WithReason, WithHint, WithCause, WithFields, WithCurrent, WithRetryAfter returning the receiver; Kind() Kind, Unwrap() error; Error() returns <code> or <code>: <detail> and never the cause or the wrapped error; LogValue() slog.Value (package log/slog) adds kind, cause and the wrapped error for logs. Package functions As(err error) (*Error, bool) and KindOf(err error) Kind (KindNone for nil, internal for any error that is not an *Error).
  • Cause is 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 ## Code listing the packages that exist. Keep the line May use the ports of these modules: ... and the section ## Tables exactly in their current form; M1-06 parses them.

cmd/bothy

  • main calls os.Exit(run(ctx, os.Args[1:], os.Stdout, os.Stderr)).
  • commands.go holds type command struct { name, summary string; run func(ctx context.Context, args []string, stdout, stderr io.Writer) int } and the table commands. This ticket registers no command; M1-07 adds config, M1-11 serve, 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 library flag only; no CLI framework (small surface, no dependency).

Lint and Sonar

  • .golangci.yml: version: "2"; linters.default: standard; enable revive (rules exported and package-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); formatters gofmt and goimports with the module path as local prefix. Never enable the exclusion preset comments. Validate with golangci-lint config verify.
  • sonar-project.properties: add **/testdata/** and **/sqlcgen/** to sonar.exclusions. Rule for all generated Go files in this repository: file names end in .gen.go (already excluded and exempt from SPDX headers in tools/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:

  • TestNewHasVersion7AndVariant
  • TestNewIsStrictlyMonotonicWithinOneMillisecond (fake clock fixed; 10 000 IDs strictly increasing in byte order)
  • TestNewNeverGoesBackwardsWhenClockDoes
  • TestNewEmbedsClockMilliseconds
  • TestNewCounterOverflowAdvancesTimestamp (random reader of 0xff bytes)
  • TestNewConcurrentUnique (64 goroutines x 1 000 IDs, no duplicate, run with -race)
  • TestParseRFC9562Example: 017F22E2-79B0-7CC3-98C4-DC0C0C07398F parses, String() is lower case, Time() is 2022-02-22T19:22:22Z.
  • TestParseRejects (table): v4 f47ac10b-58cc-4372-a567-0e02b2c3d479, v1 c232ab00-9414-11ec-b3c8-9f6bdeced846, nil, ffffffff-ffff-ffff-ffff-ffffffffffff, wrong variant 017f22e2-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 contains docs/spec, read the generated block of docs/spec/06-api.md, and assert that its code set equals AllCodes() and that each code's status equals Code.Kind().Status(). This holds the code against the real document.
  • TestEveryCodeHasKind, TestNewPanicsOnUnknownCode.
  • TestKindOf (nil, plain error, wrapped *Error via fmt.Errorf("%w")).
  • TestErrorStringHidesCauseAndWrapped, TestLogValueContainsCause.

cmd/bothy/main_test.go: TestRunWithoutArgumentsPrintsUsageAndExits2, TestRunHelpExits0, TestRunUnknownCommandExits2.

tools/archgen/*_test.go:

  • TestGenerateGolden (testdata/golden/modules.yaml to testdata/golden/go-arch-lint.yml), TestGenerateIsDeterministic.
  • TestRegisterRejects (table, one case per validation rule above).
  • TestRepositoryRegisterIsValid and TestRepositoryConfigIsCurrent (generating from ../../architecture/modules.yaml equals ../../.go-arch-lint.yml byte for byte).
  • TestRepositoryRegisterCoversModuleList: read the real docs/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: copy testdata/skeleton (its own go.mod with module example.com/skeleton, a register with modules chat, messaging, runs, and a local stub module for github.com/jackc/pgx/v5 wired by a replace directive to third_party/pgx, listed under exclude) into t.TempDir(), add the one violating file of the case, generate the config, run go-arch-lint with GOFLAGS=-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 .go files carry the SPDX line and are gofmt-clean.
  • TestArchLintTestSupport: in the skeleton, a package under test/ importing a module adapter passes; a module adapter importing a package under test/ fails with exit code 1.
  • The tests find go-arch-lint through the environment variable GO_ARCH_LINT. If it is unset they fail with set GO_ARCH_LINT to the pinned go-arch-lint (make test does this); they never skip (R-QA-02). In the root Makefile, the Go branch of test sets GO_ARCH_LINT=$(call tool,go-arch-lint) for go 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 arch exits 0 on the skeleton (go-arch-lint "No warnings found").
  • make codegen-check passes; changing modules.yaml without 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.
  • TestRegisterRejects covers every validation rule listed in Design notes.
  • The register lists every module of 02 "Module list" (TestRepositoryRegisterCoversModuleList; terminal included) with tables, actions, events, ephemeral events, ops and dependencies copied from 02.
  • kernel/ids, kernel/clock and kernel/errs tests pass with -race; TestCodesMatchChapter06 reads the real docs/spec/06-api.md.
  • Every exported symbol has a doc comment (revive exported clean); every package has a package comment.
  • Go coverage of the module is at least quality.coverage.go_min_percent; make sonar passes 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 check is 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.md entry under [Unreleased] (Added).
  • make check green.
  • Independent review over the diff (a fresh reviewer, make review) and the red team gate of docs/quality/red-team-gate.md over 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 check passes on the merged state; then git 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>.