Skip to content
SHC Docs

Architecture drift register

This is the normalization backlog for the from-scratch convergence of the SHC Go daemon. It is the consolidated output of a 12-lens parallel architecture audit run against HEAD on main: each lens established the ONE canonical pattern for its dimension, then enumerated every code deviation and every stale/false documentation claim relative to that canon. The register exists so that the codebase can be driven toward a single coherent shape as if written from scratch today — the DI composition root replacing the package-singleton service-locator, the five-tier core/scope ladder replacing the old 3-tier types.Scope* world, ent replacing every GORM/sqlc reference, core/daemonclient replacing 21 hand-rolled UDS clients, and corelog’s typed coded-error API replacing raw slog. Three sections follow: (A) STALE DOCS — every wrong doc claim grouped by file, as fixable checklist items; (B) CODE DRIFT — every code deviation grouped by dimension, sorted high→low severity; (C) ENFORCED vs PENDING fitness rules — which guards scripts/arch-fitness.sh already enforces and which proposed checks are still pending, with their greppable form. Every finding from all 12 lenses appears below.


Path convention (2026-07-31). Every internal/... path below was recorded BEFORE the B1 de-internal-ize and the CE/EE split; this repo no longer has an internal/ tree. Resolve audit-time paths per the repo convention (feature-graph.yaml header): strip the internal/ prefix — bare modules/, core/, daemon/, subsystems/, ent/ paths live in the pinned ce module; shc/...-prefixed paths are this repo, root-relative (the audit-era ee/... prefix and _ee file suffixes died in the 2026-07-31 ee/ flatten — read an old ee/X as today’s shc/X, minus the suffix). Closed/historical rows keep their audit-time paths unchanged; still-open rows below were re-pointed 2026-07-31 where the code was re-verified. Extraction-time line numbers drift — verify before use.

(A) STALE DOCS — corrections grouped by doc file

Section titled “(A) STALE DOCS — corrections grouped by doc file”

Each item: [ ] <doc:line> — <wrong claim> -> <correction>.

Output sections of older docs (cross-cutting)

Section titled “Output sections of older docs (cross-cutting)”

Superseded (2026-07-02). docs/architecture/output-contract.md is now the normative contract for every user-facing string (render modes, streams, result lines, warnings, hints, errors, exit codes, help text, logs). Output/format sections of older docs are superseded where they disagree: docs/reference/cli/overview.md was rewritten to defer to the contract, docs/reference/errors.md was regenerated (the “4xx→1, 5xx→2” exit-code claim is retired — usage errors exit 2, every runtime failure exits 1, the HTTP class travels in the -o json envelope), and any remaining per-command output notes in older docs should be fixed on touch by deferring to the contract rather than re-documented locally.

CLOSED (2026-07-04) — the DI sweep COMPLETED; every item below is moot. The Stage-C cutover landed: di-singletons is a hard 0 (func (Set|Get)RuntimeService over non-test Go returns 0), the dual-write bridge is deleted, and di-migration-plan.md now carries its own DONE/SUPERSEDED banner. ADR-0006 is COMPLETE. The pre-cutover counts below (“exactly five edges”, “233 GetRuntimeService read sites still live”, “AdapterInstaller … Stage-C not done”, “inspect/storage/connections/job never rooted”) describe a world that no longer exists — do not re-fix from these entries. (The 9 pre-cutover correction items that were here have been dropped as moot.)

DONE (2026-06-24). This doc was rewritten in commit ba09a0285; both items below are FIXED. It now states there is no GORM and no internal/models, references ent throughout, and the layer map matches the real internal/core/* tree. Do not re-fix.

  • import-dag.md:20-32,41-51,99-123 — Layer 0 = internal/core/types (Scope/Origin/StepState enums) + internal/core/errors; Layer 3 = internal/log + internal/shell; Layer 4 = internal/models; Layer 5 = internal/subsystems/{events,queue,sse,docker,database} -> None exist at HEAD: no internal/core/types (Scope is internal/core/scope/scope.go), no internal/core/errors (it is internal/core/coded), no internal/log (it is internal/core/log), shell is internal/modules/shell, no internal/models, and subsystems is only impl/ + lifecycle/ + register.go — events/docker are internal/modules/{events,docker}. The entire layer map needs rewriting against the actual internal/core/* (api,coded,config,daemonclient,db,entclient,…) tree.
  • import-dag.md:108,217-228 — Models carry “GORM model tags”; handler registration is events.Register("V200100STKINS", events.DispatchSingle, handleInstall) -> ORM is ent (entgo.io/ent v0.14.6); zero gorm in internal/. No 3-arg events.Register exists — registration uses events.DispatchSingle with an events.HandlerOptions{} struct (events/types.go:130, service_test.go:479).

docs/architecture/patterns/cross-module.md

Section titled “docs/architecture/patterns/cross-module.md”

DONE (2026-06-24). This doc was rewritten in commit ba09a0285; all four items below are FIXED. It now uses internal/core/daemonclient, says app.StackModel (formerly DeploymentModel), and explicitly notes “there is no database.GetDB” and “There is no app.HandleMoveStacks”. Do not re-fix.

  • cross-module.md:18-22,110,200 — Commands import shc/internal/daemon_client and call daemon_client.Get/GetAll/Post -> The package is internal/core/daemonclient (client.go); there is no internal/daemon_client directory. Update the import path and all examples.
  • cross-module.md:49-62,148-162BackupService.CreateForStack uses GORM (s.db.Where(...).First(&stack).Error, gorm.ErrRecordNotFound) over an app.DeploymentModel; tables reference app.DeploymentModel -> ORM is ent, not GORM (no gorm.ErrRecordNotFound path exists), and the type app.DeploymentModel was renamed to app.StackModel (internal/modules/app/models.go:50). “DeploymentModel” now appears only in code comments, never as a live type.
  • cross-module.md:86,121,192clone.NewCloneService(ctx, db); node.NewNodeService(ctx, db); database.GetDB() -> Real constructors are clone.NewService(p Providers, tenant TenantResolver) (clone/service.go:62) and node.NewService(target NodeTarget, opts ...ServiceOption) (node/service.go:124). There is no database.GetDB() function in the tree.
  • cross-module.md:78app/router.go defines func HandleMoveStacks(w, r) importing clone.NewCloneService for shc.stack.move -> No HandleMoveStacks symbol exists in internal/modules/app. The example handler name and body are stale; locate the current move handler before re-documenting.

DONE (2026-06-24). This doc was rewritten in commit ba09a0285; all three items below are FIXED. It now states “There is no package-level config.Get / GetScoped / Scope, and no types.ScopeTenant / types.ScopeLocal” and documents the five-tier core/scope ladder. Do not re-fix.

  • config.md:124-136 — Programmatic API: config.Get("collector.endpoint"), config.GetScoped("collector", types.ScopeTenant), config.GetScoped("backup.retention", types.ScopeLocal), config.GetResolved(ctx, "database.password"), config.Scope("collector.endpoint") => types.ScopeTenant -> None of config.Get/GetScoped/GetResolved/Scope exist at HEAD, and types.ScopeTenant/types.ScopeLocal were removed by the scope collapse. The scope type is scope.Scope {Global,Tenant,Environment,Stack,Deployment} (scope/scope.go:25-39) with no “Local” tier; the real entry points are config Service methods Get(ctx,path,sc Ctx) and GetAtScope(ctx,path,target scope.Scope,sc Ctx) (config/service.go:174,194).
  • config.md:76-83 — Scopes table lists only global / tenant / stack and says set/unset default to stack (“deployment-level”) -> The scope ladder now has five tiers — global, tenant, environment, stack, deployment (scope/scope.go:27-37) — and Stack outranks Environment. The table is missing the environment and deployment rows; conflating “stack” with “deployment-level” is wrong (distinct tiers; Deployment = tenant+stack+environment).
  • config.md:126,129,135 (interfaces-lens) — Code example uses types.ScopeTenant, types.ScopeLocal, config.Scope("collector.endpoint") as live API -> All three were DELETED by the scope collapse (commit 0547346f3); the dead ScopeLocal 6th tier was dropped entirely. No such symbols remain (grep in internal/modules/config + internal/core empty). Rewrite against core/scope.Scope {global,tenant,environment,stack,deployment}.

DONE (2026-06-24). This doc was rewritten in commit ba09a0285 (354-line diff); all four items below are FIXED. It now describes the composition root + the (then-)dual-write bridge instead of the pre-migration singleton model, and drops wirePhase35Seams. Note: the dual-write bridge has since been fully swept (see the di-migration-plan DONE banner), so the doc’s “resolving via GetRuntimeService” framing is itself now one cutover behind — but the original drift items are resolved. Do not re-fix from these entries.

  • daemon-manager.md:113-115 — Startup “wires the module Service singletons via daemon.wireModuleServices + daemon.wireExtraModuleServices + daemon.wirePhase35Seams” -> wirePhase35Seams no longer exists; only wireModuleServices (services.go:123) and wireExtraModuleServices (services_extra.go:76) remain, and they now populate the composition root (composition.go) under a dual-write bridge the doc never mentions.
  • daemon-manager.md:26,158,173-174 — “Clean singletons — module-level service instances”; “There is deliberately NO global service locator”; “modules publish their own runtime singletons (events.GetRuntimeService(), configmod.GetRuntimeService(), …)” as the current cross-module model -> HEAD introduced a composition root (composition.go:35-53) that explicitly names GetRuntimeService/SetRuntimeService as THE service-locator pattern being replaced by constructor injection via registry.Deps.Services. The singletons survive only as a transitional dual-write bridge; the doc presents the pre-migration model as steady state.
  • daemon-manager.md:171-172 (doc-gap, not a false claim) — Mentions “typed DepsBuilder (lifecycle.Resolver)” but never explains the one-deliberate-any contract -> Document the Resolver/DepsBuilder typing model: every shared field is concretely typed (Ctx/Pool/Config/Logger/Paths/OtelMeter/OtelMeterProvider) and ONLY Services stays any to avoid a daemon→modules→registry→daemon import cycle (registry.go:159-171). The narrowing refactor is currently undocumented.
  • daemon-manager.md:172 (interfaces-lens) — “Outside the lifecycle, modules publish their own runtime singletons (events.GetRuntimeService(), configmod.GetRuntimeService(), operations.EnsureRuntime(), …) — sibling code resolves those, never the lifecycle registry.” -> Those singletons are now EXPLICITLY-LEGACY behind a dual-write bridge (composition.go:35-49). Canonical path is constructor injection: consumers read their service via registry.Deps.Services cast to a consumer-defined xServices interface (schedule/register.go:50-72), GetRuntimeService only as fallback. Describe *daemon.Services as the composition root and the singletons as the migration bridge.

DONE (verified 2026-07-17). The 2026-06-24 “STILL STALE” banner that stood here is itself stale: diagrams/README.md now cites internal/core/scope/ + internal/core/coded/, and grep of ALL 14 .mmd sources for internal/core/types|internal/core/errors|internal/models returns zero matches (including the two named offenders).

  • README.md:68 — Diagram 14 graphs “the package dependency graph from internal/core/types/ and internal/core/errors/ … up to the modules” -> Neither dir exists at HEAD; the foundational error package is internal/core/coded and the scope type is internal/core/scope. The .mmd sources (14-import-architecture.mmd, 16-cycle-elimination.mmd) carry the same dead paths. (DONE 2026-07-17: README + both .mmd sources cite scope/coded.)

docs/architecture/patterns/communication-patterns.md

Section titled “docs/architecture/patterns/communication-patterns.md”
  • communication-patterns.md:687 — “Task model | internal/models/task.go (TaskModel)” -> internal/models does not exist; TaskModel is defined at modules/task/models.go (ce module). (DONE 2026-07-31, this sweep: the key-files table now cites modules/task/models.go + ent/schema/task.go; the page’s Pattern 5 RPC section was also rewritten against modules/internal_rpc + the peer client, and the dead progress/rpc/internal_api paths were replaced.)
  • schedule.md:127 — “ScheduleModel (internal/models/schedule.go)” -> internal/models does not exist; ScheduleModel is defined at modules/schedule/models.go (ce module). (DONE 2026-07-31, this sweep: repointed, and the page’s SyncSchedules-in-app claims were rewritten to modules/schedule/service.go::SyncApp.)
  • README.md:172 — Backend standards links to docs/contributing/standards/backend/models.md and .../backend/queries.md -> Those files do not exist (the backend dir has only api.md and migrations.md); both links are broken (404). Remove them or create the targets. (DONE 2026-07-17: the root README row now links only backend/api.md + backend/migrations.md; the same dead links had been copied into docs/contributing/README.md and were removed there in the same sweep.)
  • tech-stack.md:62 — Query layer = “database/sql + per-module repos” (implying database/sql only) -> ent is ALSO a runtime query client (entclient.Get() used in schedule/service.go:112, connections/repository.go:40, audit, cluster, vault); the data layer is database/sql repos AND a live ent client. (DONE 2026-07-17: the row now reads “ent ORM + per-module repos | ent-generated client (ent/) over the shared db pool”.)
  • tech-stack.md:202 (logging-lens) — “Logs | slog wrapper in internal/core/log/ (structured JSON + audit class)” -> Understated: omits that the wrapper is a TYPED, coded-error-mandating API (raw slog is banned). (DONE 2026-07-31, this sweep: the row now states WARN+ requires a *coded.CodedError, raw slog is lint-banned, and names the event-bus fan-out.)

daemon/composition.go (ce module) — godoc / accessor comments

Section titled “daemon/composition.go (ce module) — godoc / accessor comments”

Re-verified 2026-07-31 — still live. daemon/composition.go:58 still says “wave 1 — leaves … More land here”, and the docker accessor comment (now ~:559) still names a docker.resolveService that does not exist (no resolveService in modules/docker or modules/clone). Lines have drifted from the audit-time anchors below.

  • composition.go:57 — “wave 1 — leaves (no cross-module dependency). More land here as each wave converts; see the migration plan.” -> W3 (app+clone, commit cf6dd4199) was the LAST producer wave; no further fields land on *Services (Stage C only deletes dual-writes). Also inspect/storage/connections/job were planned for conversion but never added to the root. Reword to “all producer waves (W1a/W1b/W1c/W3) have landed; remaining migration work is Stage C consumer cutover, not new fields” and drop “More land here as each wave converts.”
  • composition.go:378-387 — DockerService() doc: “The injection point docker.resolveService reads instead of docker.GetRuntimeService().” -> docker has no resolveService and no injected read path; docker.NewRouter ignores Deps (router.go:27). Reword: DockerService() is published for a future consumer but ALL current docker.Service reads go through docker.GetRuntimeService() (the bridge) — accessor is currently write-only.
  • composition.go:936-945 — CloneService() doc: “The injection point clone.currentResolver reads instead of clone.GetRuntimeService() per-request. The clone module’s HTTP router … resolves through the package serviceResolver.” -> Self-contradictory and false: clone.currentResolver() reads the package-global resolveSv, never d.Services (clone/router.go:54). CloneService() is write-only; clone reads stay on the package serviceResolver/GetRuntimeService bridge.
  • composition.go:104 (interfaces-lens) — Block at 100-109 / 936-973 describes clone as converted “to the composition root — W3 (last DI wave)” with CloneService() as the injected read path -> Producer-side only. The clone router never reads d.Services/CloneService() (router.go:72 ignores Deps; resolves via package currentResolver()); the only CloneService() reader is the daemon reading its own field (services_seams_clone.go:230). State that clone’s consumer-read seam is NOT yet on the root, unlike the 21 xServices modules.

subsystems/lifecycle/registry.go (ce module)

Section titled “subsystems/lifecycle/registry.go (ce module)”
  • (re-verified 2026-07-31 — still live, now at :61) registry.go:57 — “Nil means “Start with a nil deps argument” — useful for self-contained subsystems like bootstrap.” -> bootstrap is no longer self-contained: bootstrap.go:65-71 registers a non-nil DepsBuilder reading r.Logger into BootstrapDeps. Replace the example with a subsystem that still registers a nil DepsBuilder, e.g. operation_registry or schedule_dispatcher.
  • (re-verified 2026-07-31 — still live, now at :22) models.go:20 — “a non-NULL tenant_name with NULL stack_name marks a tenant-scoped row; both non-NULL marks a stack-scoped row.” (3-tier global/tenant/stack model) -> ConfigModel now has Environment too, and Scope() classifies into FIVE tiers (Global/Tenant/Environment/Stack/Deployment) via scope.Classify (models.go:36-52). Enumerate all five tiers or defer to the Scope() godoc.
  • (re-verified 2026-07-31 — still live, now at :774) service.go:505 — “matching the legacy scopeToSecretScope case \"\": fallthrough.” — names a function that no longer exists -> scopeToSecretScope was removed in the scope-type collapse (only core/scope remains). Drop the dangling symbol; describe the behaviour directly: “empty req.Scope defaults to the most specific (deployment) tier.”
  • (re-verified 2026-07-31 — still live: paths.go:270 still mirrors Python, core/scope/scope.go:15 still mirrors vault) paths.go:147 — “Mirrors PathBuilder.get_cascade_paths in Python.” (paired with scope.go:16-18 claiming the canonical cascade “mirrors the historical vault path cascade (internal/modules/vault/paths.go)”) -> core/scope.Cascade is now the single authority; vault CascadePaths duplicates its ordering by hand rather than calling it. Update both comments to name internal/core/scope.Cascade as the source instead of mutually referencing each other / Python.

Re-verified 2026-07-31 — both rows still live (the “13-character” prose and the registered-at-init claim are unchanged at HEAD).

  • coded.go:9 / :35 — “The 13-character code is parsed as X<HTTP3><SEQ3><MNEMONIC6>” / “Code … 13-char identifier” -> The code is NOT fixed at 13 chars. The regex is ^X[0-9]{6}[A-Z]{6,}$ (X + 6 digits + 6-OR-MORE uppercase); coded.go:27-28 + TestNewAcceptsLongMnemonic (coded_test.go:55) document 14-char legacy codes like X200049LHRFRSH. The panic at coded.go:125 says “want X<6digit><6upper>”. Say “X + 6 digits + 6+ uppercase”.
  • coded.go:5-7 — “Factory functions named X######CCCCCC produce one and are registered into the process-wide registry at init() time.” -> Registration is NOT automatic from the factory — it depends on a SEPARATE, hand-maintained errorSpecs/coreErrorSpecs slice kept in sync. 3 factory codes (X500440BKPSDR, X503056COLPRT, X503999RTENYI) have a factory but are never registered, so the guarantee does not hold. (Re-verified 2026-07-02: of the 6 originally listed, X200063BKPQSC and X500017EVTPRN gained registered Spec entries and X200971VLTVAD was deleted outright.)

core/coded/boundary.go (ce module) — re-verified 2026-07-31, claim still present at :19-20

Section titled “core/coded/boundary.go (ce module) — re-verified 2026-07-31, claim still present at :19-20”
  • boundary.go:18-21 — “Each module’s repository layer is expected to wrap calls here [coded.FromExternal] so the helper that escapes the module always carries a coded error.” -> FALSE. 0 of the 9 *repo*.go/repository.go files call FromExternal. All 65 FromExternal call sites live in command/service/provider code (backup, export, job/commands, api/auth.go), not the repository layer. The repository layer returns sql.ErrNoRows-wrapping coded errors via factories, not FromExternal.
  • (re-verified 2026-07-31 — still drifted: 8 factories vs 6 Spec entries now) errors.go:109-111 — “Keep this list in lockstep with the factories above — init() below registers each Spec into the coded registry too.” -> The list is NOT in lockstep: 8 factories but stackErrorSpecs has only 5 entries. X400955RTMPIN, X500907STKFAL, X503999RTENYI are factory-only and never registered. The comment asserts an invariant its own file violates.

modules/internal_stacks/errors.go (ce module)

Section titled “modules/internal_stacks/errors.go (ce module)”
  • (re-verified 2026-07-31 — the divergent X500071..X500075 factories are still there) internal_stacks/errors.go (whole file, 78 lines) — Implicitly presents X500071DEPFAL..X500075PSFAIL as the module’s own coded errors with no errorSpecs/init (zero Register calls) -> These 5 codes are actually owned, specced and registered in internal/core/api/errors.go:180 with DIFFERENT signatures/templates. The internal_stacks factories are an unregistered, divergent second implementation in the wrong layer; rendered messages never match the registered Spec.
  • .golangci.yml:114-120 (forbidigo header comment) — “every error returned across a function boundary must be a *coded.CodedError … Plain errors.New/fmt.Errorf … are banned in production paths.” -> The rule is real but was UNENFORCED on three live boundary-escaping sites. (Re-triaged 2026-07-31: two of the three are clean at HEAD (no fmt.Errorf left in ce daemon/job_executor_adapter.go or modules/app/commands/system_install.go); the hud validation-closure site survives as shc/modules/hud/install_flow.go:118 — verify whether it still lacks its siblings’ //nolint before closing.)

docs/contributing/engineering-standards.md

Section titled “docs/contributing/engineering-standards.md”
  • (DONE 2026-07-31, this sweep: 2.16.1 now teaches corelog.Warn(ctx, <coded>) / corelog.Error(ctx, <coded>, cause) with the real X200003OPFAIL/X500907STKFAL signatures and states raw slog.Warn/Error are lint-banned.) engineering-standards.md:180-192 — Section 2.16.1 “Logger API — the only shapes allowed” prescribed slog.Warn("operation failed", "error_code", X200003OPFAIL.Code(), ...) / slog.Error(..., "error_code", X500907STKFAL.Code(), ...) as the only-allowed shapes -> This is the EXACT pattern the codebase BANS. Per internal/core/log/typed.go:15-21 and .golangci.yml:132-135, slog.Warn/slog.Error are forbidden in production; canonical form is corelog.Warn(ctx, X200003OPFAIL()) / corelog.Error(ctx, X500907STKFAL(), cause) passing the *coded.CodedError itself (a string is a COMPILE error). The doc teaches the banned form, the wrong signature (.Code() string attr vs typed object), and the example codes X200003OPFAIL/X500907STKFAL are not real.

docs/contributing/standards/error-handling.md

Section titled “docs/contributing/standards/error-handling.md”
  • error-handling.md:164 — Best practice #4 “Use structured logging — use slog with attributes” -> Production paths must use corelog’s typed API (corelog.Warn/Error with a *coded.CodedError), not raw slog. Bare slog.Warn/slog.Error are forbidigo-banned (.golangci.yml:132-135). Should read “use corelog.Warn(ctx, ) / corelog.Error(ctx, , cause)”. (DONE 2026-07-17: best practice #4 — now near error-handling.md:205 after a restructure — reads “Use the typed logging API — corelog.Warn(ctx, ) / corelog.Error(ctx, , cause); raw slog.Warn/slog.Error are lint-banned in production paths”.)
  • (DONE 2026-07-31, this sweep: the severity list now reads DEBUG/INFO/WARN/ERROR with a corelog.Fail note.) observability.md:80 — Listed log severities as TRACE, DEBUG, INFO, WARN, ERROR, FATAL -> corelog/slog support only DEBUG/INFO/WARN/ERROR. There is no LevelTrace or LevelFatal in internal/core/log (grep: zero). corelog.Fail (typed.go:62) logs at LevelError then os.Exit — not a distinct FATAL severity. TRACE and FATAL are fictional.

docs/contributing/standards/coding-style.md

Section titled “docs/contributing/standards/coding-style.md”
  • (DONE 2026-07-31, this sweep: the line now names corelog, and notes WARN+ text lives in the coded factory template.) coding-style.md:95 — Referred to message-string rules applying to shclog.Debug/Info/Warn message arguments -> Uses the holdover shclog alias; the mandated alias is corelog. corelog.Warn takes a *coded.CodedError, not a free-text “message argument” — the human string lives in coded.New’s template, not at the corelog call site. The phrasing implies a free-text slog-style signature that does not exist for Warn.

core/api/server.go (ce module) — Phase-3 stale comments

Section titled “core/api/server.go (ce module) — Phase-3 stale comments”
  • server.go:262-263 — “Phase 3 wires this into the lifecycle watchdog. For now stderr is the canonical place.” -> (RESOLVED by HEAD: core/api/server.go no longer writes to os.Stderr at all — the raw listener-exit writes are gone. Verified 2026-07-31.)
  • (re-verified 2026-07-31 — still live at :124) server.go:124 — ”// Phase 3 will mount module routers here via registry.Routers().” (future tense) -> Module routers ARE mounted today — by the unix_socket subsystem (internal/subsystems/impl/unix_socket.go:135), which walks registry.Routers() and flattens onto srv.PublicRouter(). Phase 3 is long complete; point at the unix_socket/internal_api flatten.
  • (re-verified 2026-07-31 — still live at :129) server.go:129 — ”// listeners. Phase 3 callers attach module sub-routers here.” (on PublicRouter()) -> Stale. PublicRouter() is populated by the unix_socket subsystem’s chi.Walk flatten (Method per leaf), not by callers mounting sub-routers; sub-router Mount was explicitly abandoned (“chi doesn’t allow Mount(”/”,…) twice”).

internal/modules/*/register.go & router.go (stale daemon.go flatten pointers + lazy/eager claims)

Section titled “internal/modules/*/register.go & router.go (stale daemon.go flatten pointers + lazy/eager claims)”
  • sysroutes/router.go:42 — “The daemon flattens this onto the public mux via chi.Walk at startup (internal/daemon/daemon.go:startAPIServer).” -> (RESOLVED by HEAD: the comment now names “the unix_socket / http lifecycle subsystems” (modules/sysroutes/router.go:59-60), and startAPIServer is deleted from the tree. Verified 2026-07-31.)
  • (re-verified 2026-07-31 — still live, now at modules/app/router.go:143; the live call is subsystems/impl/unix_socket.go:183) app/router.go:99-100 — “/docs, /docs/oauth2-redirect, /openapi.json are wired by the daemon (daemon/daemon.go) via api.RegisterOpenAPIRoutes against the public mux” -> RegisterOpenAPIRoutes moved into the unix_socket subsystem during WIRE4. The daemon.go call site is gone (startAPIServer deleted) — the comment now dangles entirely.
  • (re-verified 2026-07-31 — still live at modules/app/register.go:91) app/register.go:89 — “Return the chi router directly so chi.Walk in daemon.go can enumerate routes for /internal/_manifest.” -> chi.Walk for the public mux + manifest runs in the unix_socket subsystem, not daemon.go (startAPIServer is deleted).
  • app/register.go:84-87 — “The Router and Commands builders both resolve the Service lazily via GetRuntimeService” -> (RESOLVED by HEAD: the comment now honestly documents build-time resolution off the composition root (appServiceFromDeps on registry.Deps.Services) with per-handler nil-checks. Verified 2026-07-31.)
  • (re-verified 2026-07-31 — still live at modules/backup/register.go:43) backup/register.go:35 — “Return the chi router directly so chi.Walk in daemon.go can …” -> Live chi.Walk is in the unix_socket (public) / internal_api (internal) subsystems, not daemon.go.
  • (re-verified 2026-07-31 — still live at modules/audit/register.go:38) audit/register.go:38 — “so chi.Walk in daemon.go can enumerate the routes for the …” -> Stale daemon.go pointer; the enumerating walk is in the unix_socket / internal_api subsystems.
  • connections/router.go:21-29 — “GetRuntimeService() is nil in a plain shc provider … resolved LAZILY per-handler via GetRuntimeService()” -> (RESOLVED by the DI sweep: modules/connections/router.go now resolves off the composition root — resolveService(d coreregistry.Deps) reads registry.Deps.Services. Verified 2026-07-31.)
  • registry.go:130 / daemon.go:697-700 comment — the dead-startAPIServer narration -> (RESOLVED: startAPIServer and its flatten copy are deleted from the tree; the sole flatten narration lives in subsystems/impl/unix_socket.go. Verified 2026-07-31.)

internal/modules/app/state.go & state_mirror.go (state-persistence narration) — RESOLVED (ADR-0002 / B1)

Section titled “internal/modules/app/state.go & state_mirror.go (state-persistence narration) — RESOLVED (ADR-0002 / B1)”

The state-model collapse landed: state_mirror.go is deleted, the in-memory app.State is demoted to the offline/test store (selected only when no StackRepo is wired), and the app Service routes every stack read/write through a single active deploymentStore — the DB deployment table on the daemon, memory offline. The disk-sweep is a one-shot readiness-gated ImportOrphanManifests boot pass (no perpetual ticker, no tombstone).

  • app/state.go:11-16 — the write-through-cache narration is rewritten to the ADR-0002 end state: State is the OFFLINE/TEST authority only, NOT a cache the DB populates and NOT a mirror source.
  • app/state_mirror.go:3-11 — file DELETED; writes target the StackRepo directly through the Service’s active deploymentStore (the stack.Service.recordStackRow shape).
  • app/state_mirror.go:27-34ReconcileDiskStacks (perpetual ticker + tombstone) replaced by the one-shot ImportOrphanManifests boot import; the out-of-band clone/move/recover writers call RegisterRecoveredStack (now a direct store write) at operation time.
  • services_seams_clone.go:300-303 — the disk-only TODO is retired: the post-clone reconcile is ImportOrphanManifests (idempotent safety net) and clone/move/recover write the deployment row directly via RegisterRecoveredStack.
  • stack/router.go (handleStatus) + app/router.go (stackStatusHandler) — single-owner confirmed: stack/router.go cedes shc.stack.status and app/router.go owns it (unchanged by B1).
  • testing.md:16 — Unit tests described as running against “fakes / mocks” -> Change to “fakes”. (DONE 2026-07-31, this sweep.)
  • testing.md (whole file) — The canonical Testing Standards doc never mentions the DB test-backend model -> It omits the cluster-parity test driver (dbtestreg wraps modernc sqlite and rejects DML+RETURNING, matching the rqlite cluster engine’s /db/execute path) and the dbtestreg side-effect import every DB-touching package needs. This is the single most load-bearing testing convention and is undocumented.
  • (DONE 2026-07-31, this sweep: the snippet now points at make test/integration and the enumerated coverage paths.) testing.md:83go test -race ./... given as a how-to-run command -> ./... walks the dh-golang obj-*/ GOPATH tree (the Makefile enumerates paths explicitly at :306/:315/:320 to dodge it), and -race here is also not how the suite enables it (integration lane probes and applies $(RACE), Makefile:165-173/:344). Replace with the enumerated make test/integration form.
  • Makefile:334 and :346 — Comments asserted “tests/integration/ has no Go packages yet” -> STALE: tests/integration/ contains build-tagged Go test packages. (DONE 2026-07-31, this sweep: both the comment and the skip message now describe the go list guard neutrally.)
  • state.go:14-16 — “Tests substitute a fake by calling SetRuntimeService directly” -> (RESOLVED by the DI sweep: the comment block was rewritten — state.go now documents pure helpers + composition-root resolution and prescribes no global swap. Verified 2026-07-31.)

migration/decisions/B7-module-registration.mdMOOT (file deleted)

Section titled “migration/decisions/B7-module-registration.md — MOOT (file deleted)”

The migration/decisions/ tree was retired with the migration-era docs; neither B7-module-registration.md nor B3-module-boundaries.md exists in either repo at HEAD (verified 2026-07-31). The rows below are closed with the files.

  • B7-module-registration.md:28-30,109 — Shows each module’s init() calling registry.Migrations("<name>", embeddedMigrations) (alembic dir → embed.FS) and main() calling database.MustMigrate(registry.Migrations()) -> ZERO modules call registry.Migrations (only the setter def + comments match). Schema migrations are owned by ent (the /ent dir) and run via the impl.NewMigrations() subsystem, not a per-module embed.FS. The registry.Migrations setter/map is effectively dead code.
  • B7-module-registration.md:34 (and types.go Subsystem interface doc) — Shows registry.Subsystem("<name>", lifecycleHooks) in each module’s init() for “runtime-state modules” -> No registry.Subsystem SETTER exists in internal/core/registry/registry.go (only an unused subsystems map + getter). Subsystems are registered via internal/subsystems/register.go RegisterAll() listing impl.NewXxx().Registration() into the lifecycle registry. No module’s init() registers a subsystem.

migration/decisions/B3-module-boundaries.md

Section titled “migration/decisions/B3-module-boundaries.md”
  • B3-module-boundaries.md:26 — “models.go — sqlc-generated row structs (read-only) plus any persisted DTOs.” -> The codebase uses ent (entgo.io), not sqlc — the /ent dir is at repo root; modules import ent.Client (ca/store.go:150 EntClient() *ent.Client). grep for ‘sqlc’ returns no Go usage. models.go files hold ent-backed repos/DTOs.
  • B3-module-boundaries.md:194-204 (B3.6) — “Modules register their cron/job definitions with internal/core/registry/jobsjobsRegistry.Add(...) against the central registry” -> There is no internal/core/registry/jobs package. Crons are registered via registry.Crons(name, []CronJob) into registry.go:65. The ‘jobs’ sub-package and ‘jobsRegistry.Add/Remove’ API do not exist.
  • B3-module-boundaries.md:140-155 (B3.4) and 78-80 (B3.1) — “create internal/core/docker containing Env() (formerly get_docker_env), ComposeUp/Down/Stop/Exec, SyncContainerStatus … All modules import internal/core/docker” -> internal/core/docker does NOT exist (only modules/docker). The compose driver lives at internal/modules/app/compose_driver.go and DOCKER_HOST is constructed in node/service_extras.go + health/reconcile — the B3.4 core/docker extraction was never done.
  • B3-module-boundaries.md:127-136 (B3.3) and 104-109 (B3.2) — “container-status mutators move to internal/modules/stack/service.go as methods on StackService” and “Shared response DTOs (StackResponse, StackListResponse, StackStatusResponse) live in internal/modules/stack/types.go. app imports them.” -> UpdateContainer/RemoveContainer still live in app (app/recorder.go:76,150) with container_repo.go/container_read.go/container_status.go all in app. StackResponse is defined in app/types.go:246, NOT stack/types.go (only StackStatusResponse made it to stack/types.go:253). The app↔stack split is only partially executed.

modules/tenant/register.go (ce module) + the 20 “DI template” comments

Section titled “modules/tenant/register.go (ce module) + the 20 “DI template” comments”
  • (re-verified 2026-07-31 — the “DI template every module router follows” cross-reference is still there, now at :46) tenant/register.go:50 (and 20 copy-pasted “This is the DI template every module router follows (see internal/modules/user/router.go)” comments) -> There is no single template: user/router.go puts the seam in router.go with a *routes struct + newRoutes; tenant/register.go puts it in register.go with a free resolveService + newRouter. The 20 files implement at least 4 distinct shapes (interface-in-register vs interface-in-router; *Service vs func() *Service resolve; newRouter free-func vs *routes struct). The cross-reference is misleading.
  • (re-verified 2026-07-31 — still live at :17-18) operation/router.go:18-19 — ”// ModuleName is the canonical registry key. Matches the Python module path tail (shc.progress).” while the const value is “operation” -> The const ModuleName = "operation" does NOT match “shc.progress”; the comment’s claimed Python tail and the actual registry key disagree (operation vs progress).

core/daemonclient/client.go (ce module) + duplicate “kept in sync” comments

Section titled “core/daemonclient/client.go (ce module) + duplicate “kept in sync” comments”

Re-triaged 2026-07-31: the four “Kept in sync with client_wiring.go” comments (vault/ca/connections/ctx rows below) are GONE at HEAD — those rows are closed. The package-doc partial-centralisation claim needs a fresh count before verdict.

  • daemonclient/client.go:1-24 — Package doc says it “centralises three things that used to be duplicated across internal/cli/client_wiring.go and the audit/repo/tenant module register.go files” and modules “can now import this package” — implying the duplication is resolved -> STALE/MISLEADING: only cli + audit/repo/tenant/backup/config/clone/schedule/inspect migrated. The SAME dialer+resolver+isSocket is STILL hand-rolled in 13+ live files (vault, ca, connections, shell, logs, node/commands, cluster/commands, task/commands, storage/commands, health/commands, app/commands, app/register, events/commands, job/register, ctx). Say the centralisation is PARTIAL and name the un-migrated modules.
  • (comment deleted at HEAD — verified 2026-07-31) vault/http_client.go:39 — “daemonSocketCandidates … Kept in sync with internal/cli/client_wiring.go.” -> client_wiring.go no longer declares daemonSocketCandidates — it delegates to daemonclient (client_wiring.go:46 type daemonClient = daemonclient.Client). The real source of truth is daemonclient/client.go:51.
  • (comment deleted at HEAD — verified 2026-07-31) ca/daemon_client.go:39 — “caDaemonSocketCandidates … Kept in sync with internal/cli/client_wiring.go and the vault module.” -> Same staleness: client_wiring.go declares no candidate slice. Comment points at a file that no longer holds the list.
  • (comment deleted at HEAD — verified 2026-07-31) connections/daemon_client.go:39 — “connectionsDaemonSocketCandidates … Kept in sync with internal/cli/client_wiring.go and the ca/vault modules.” -> client_wiring.go holds no such list. The “kept in sync” invariant is unenforceable; canonical source is daemonclient/client.go:51.
  • (comment deleted at HEAD — verified 2026-07-31) ctx/runtime.go:242 — “daemonSocketCandidates mirrors internal/cli/client_wiring.go …” -> client_wiring.go no longer contains the slice; the live canonical is daemonclient/client.go:51.
  • config/types.go:29-32 — scopeMap doc pointed only at the retired Python file -> (RESOLVED at HEAD: the comment now names core/scope as the canonical ladder and frames scopeMap as the input-synonym table. The shared-Parse() dedup itself is still tracked by Lens 11’s NO-SCOPE-SYNONYM-SWITCH proposal. Verified 2026-07-31.)
  • (2026-07-31: needs re-verification — sibling orderings were not re-checked this pass) cluster/commands/daemon_client.go:7-13 — “The resolution order matches the app helpers: 1. SHC_SOCKET_PATH 2. runtime-dir 3-4. well-known 5. SHC_URL.” -> Does NOT match all siblings: node/commands/daemon_client.go puts SHC_URL FIRST (node/commands/daemon_client.go:6 “SHC_URL always wins”), and events/commands/main.go omits the runtime-dir probe. The “uniform order” claim is false.

Subsystem lifecycle godoc (subsystems/lifecycle/* + subsystems/impl/*, ce module)

Section titled “Subsystem lifecycle godoc (subsystems/lifecycle/* + subsystems/impl/*, ce module)”
  • lifecycle/types.go:9 — “…start concurrently via errgroup.” -> (RESOLVED at HEAD: the godoc now says “hand-rolled goroutines feeding a buffered result channel in startGroup (NOT errgroup; the package never imports it)” and documents the same-priority Deps gating. Verified 2026-07-31.)
  • (re-verified 2026-07-31 — the Stop-honours-Deps claim is still there at :17) lifecycle/types.go:8-10 — “Stop walks the reverse order, also honouring Deps (A depends on B → A stops before B).” -> Stop/shutdownAll (lifecycle.go:371-381) walks priority GROUPS in reverse, it does not honour Deps within/across groups — same-priority members stop concurrently regardless of any dep. Only true insofar as topoSort ordered groups by priority at start.
  • (re-verified 2026-07-31 — “Single|” still in the godoc at :17) lifecycle/subsystem.go:17-19 — “Subsystem.Modes … returns the bitmask of run modes (Single|ClusterFollower|ClusterLeader)” -> There is no Single mode. The Mode type (types.go:34-43) defines only ModeClusterFollower and ModeClusterLeader (plus ModeAny union). types.go:30-31 documents that Single was removed. Drop “Single|”.
  • (re-verified 2026-07-31 — still live; rollback() still walks the running slice) lifecycle/subsystem.go:11 / :30 — “on shutdown it walks Stop in reverse-priority order.” and ”:30 non-nil triggers rollback in reverse-priority order.” -> Stop walks reverse-priority (true), but ROLLBACK (mid-startup-failure path, lifecycle.go:451-484) walks reverse START order via the running slice, NOT reverse-priority (lifecycle.go:454-456; lifecycle_test.go:188 asserts “b stopped before a”). The line-30 rollback claim is wrong.
  • (re-verified 2026-07-31 — still live, now ~:111) lifecycle/lifecycle.go:112 — Wire doc: “on the first Start error every already-Running subsystem is rolled back in reverse-priority order.” -> rollback() walks the running slice in reverse (reverse START order), not reverse-priority. Should read “in reverse start order”.
  • impl/base.go:1-4 — “…one of the 23 subsystems listed in B8’s catalog.” -> (RESOLVED at HEAD: base.go now says 32-and-growing with the B8-was-23 history note, and the shared count is baseCatalogSize = 33 in subsystems/register_edition.go — the count is code now, not prose. Verified 2026-07-31.)
  • impl/docker_events.go:54-56 — Start doc normalised the Start-ctx capture -> (RESOLVED by the Lens 9 reroot: the godoc now documents the daemon-lifetime rooting and calls the Start-ctx capture a latent landmine explicitly; subsystem-loop-start-ctx enforces the code side. Verified 2026-07-31.)
  • impl/operation_registry.go:26-28 — presented OnLeader-ctx as the shutdown driver -> (RESOLVED by the Lens 9 reroot: the godoc now states the goroutine “roots on the daemon-lifetime ctx (not the ephemeral OnLeader ctx)”. Verified 2026-07-31.)
  • nebula/rotation.go:188-190 — Start doc endorsed caller-ctx lifetime -> (RESOLVED by the Lens 9 reroot (see that lens’s banner); nebula rotation now lives at shc/modules/nebula/ and the loop rerooting is enforced by subsystem-loop-start-ctx. Verified via the fitness gate 2026-07-31.)

INVENTORY SNAPSHOTS DELETED (2026-07-29). The per-surface extraction snapshots that lived under docs/architecture/feature-inventories/ were retired — the graph YAML is the sole surviving artifact, hand-refreshed directly against HEAD. The hud.yaml/cli.yaml drift items that sat here are moot with the files; only the graph-YAML residue below remains. Trust the code at HEAD over extraction-time anchors.

  • feature-graph.yaml (hud errors surface) — stale hud error-code count -> (RESOLVED 2026-07-30 by the feature-graph citation repoint (b5f0a79aa): every module X-code aggregate row — hud included — was re-synced to docs/reference/errors.md counts and per-module source dirs.)

(B) CODE DRIFT — deviations grouped by dimension, sorted high → low severity

Section titled “(B) CODE DRIFT — deviations grouped by dimension, sorted high → low severity”

Each item: [ ] <severity> <where> — <what>.

(All findings in this lens are documentation errors; see Section A. No code-side deviations beyond the doc claims.)

Lens 2 — godoc-comments (DI / lifecycle / scope packages)

Section titled “Lens 2 — godoc-comments (DI / lifecycle / scope packages)”
  • medium internal/daemon/composition.go:57-58 — Services-struct comment “wave 1 — leaves … More land here as each wave converts” is stale: W3 (cf6dd4199) was the LAST producer wave (Stage C deletes dual-writes, adds no fields); and inspect/storage/connections/job (named for conversion in the plan) were silently dropped from producer scope.
  • medium internal/subsystems/lifecycle/registry.go:56-57 — Resolver.DepsBuilder godoc cites bootstrap as the “Start with a nil deps argument” exemplar, now FALSE (bootstrap.go:65-71 has a non-nil DepsBuilder building BootstrapDeps{Logger: r.Logger}). The general clause is fine, but the named example is wrong.
  • medium internal/modules/config/models.go:20-22 — ConfigModel field doc describes only the OLD 3-tier scope world; omits the Environment column and the Environment/Deployment tiers. The very next method Scope() (models.go:36-52) correctly documents all FIVE tiers.
  • low internal/modules/vault/service.go:505 — godoc references scopeToSecretScope (“the legacy … fallthrough”), a symbol no longer defined anywhere in non-test code (collapsed into core/scope). Dangling symbol a reader cannot locate.
  • low internal/modules/vault/paths.go:146-178 — Duplicate-impl drift: scope.go:16-18 and vault paths.go:146-147 still mutually “mirror” each other / Python, but vault CascadePaths now re-implements scope.Cascade’s tier ordering by hand instead of calling it. The canonical authority is core/scope; neither comment names it.

RESOLVED (2026-07-04) — DI sweep COMPLETE. di-singletons is a hard 0 and the dual-write bridge is deleted (ADR-0006 COMPLETE); composition.go is constructor-injected. The write-only accessors, un-rooted connections/job singletons, the “29 bridge calls”, and the forward write-only accessor enumerated below were all closed by the completed sweep. Historical — do not re-fix.

  • high internal/daemon/composition.go:378-392 (claim) vs internal/modules/docker/router.go:27 — DockerService() comment claims docker.resolveService reads the injected root; no such function exists and docker NEVER reads d.Services (NewRouter(_ registry.Deps) ignores Deps). The accessor is WRITE-ONLY.
  • high internal/daemon/composition.go:936-950 (claim) vs internal/modules/clone/router.go:51-54 — CloneService() comment claims clone.currentResolver reads the injected root per-request; it reads the package-global serviceResolver (resolveSv), not d.Services. Accessor is WRITE-ONLY, identical to docker.
  • medium internal/modules/connections/state.go:10, internal/modules/job/state.go:25 — Incomplete-conversion drift: connections.Service and job.Service keep their OWN Set/GetRuntimeService singletons off the composition root, while their W2-sibling metrics IS on the root. The W2 commit (252eef583) converted the vault edges but left two of three module singletons un-rooted — asymmetric, undocumented.
  • medium internal/modules/operation/registry.go:189, internal/modules/inspect/register.go:34, internal/modules/backup/router.go:400 — Un-migrated module singletons the plan lists as Wave-1 leaves: operation (SetRuntimeRegistry), inspect, stack/node (resolver-only). storage is now FULLY SWEPT (SetRuntimeService/GetRuntimeService + the router serviceResolver deleted; field+StorageService()+setStorage on the root; the no-Deps doctor + storage.shared_op RPC seams resolve the root per-call). The backup→operation edge is still a raw operation.GetRuntimeRegistry() (backup/router.go:400,483).
  • low internal/daemon/composition.go (29 bridge calls at 131,149,167,185,209,230,251,275,301,343,373,413,451,489,526,567,603,643,677,735,767,785,812,830,858,884,908,933,972) — Bridge scaffolding still fully present: 29 dual-write legacy-singleton calls (24 distinct setters; app carries 4) + 233 GetRuntimeService read sites across internal/. Stage-C scaffolding to delete; pin the count so no NEW bridges are added.
  • low internal/modules/forward/register.go:153 vs internal/daemon/composition.go:197 — forward.RunForward (and schedule commandAdapter) still resolve via the legacy singleton because they receive no registry.Deps. composition.go documents this honestly, but ForwardService() accessor is effectively write-only today.
  • high internal/modules/backup/errors.go (115/114); internal/modules/internal_stacks/errors.go:1-78 (5/0); internal/modules/stack/errors.go:112-138 (8/5); internal/core/coded/errors_core.go:1153+1311 (155/155) — errorSpecs DOUBLE-ENTRY: every code is written 3x (factory, Spec, sometimes test) with no generic test that factory template/params match the Spec. 8 modules drifted: backup 115/114, internal_stacks 5/0, stack 8/5, vault 32/31, subsystems/impl 53/52, events 8/7, health 4/3, repo 8/7.
  • high modules/stack/errors.go vs core/api (ce) — X503999RTENYI duplicate. (Re-verified 2026-07-31: the code is now REGISTERED with a {feature} not yet wired Spec in core/coded/errors_core.go:414 — the never-registered half is fixed; whether a divergent bare-coded.New copy survives in core/api needs a fresh grep before closing.)
  • high core/coded/errors_core.go vs modules/health/errors.go (ce) — X200003OPFAIL has TWO factories. (Re-verified 2026-07-31: both factories now share the SAME 3-arg (operation, output, cause) signature — the conflicting-signature half is gone; the duplicate definition itself remains.)
  • high internal/core/api/errors.go:54-… vs internal/modules/internal_stacks/errors.go:22-78 — X500071DEPFAL + X500072STPFAL + X500073LOGFAL + X500074EXCSRV + X500075PSFAIL each have TWO divergent factories: core/api (registered at errors.go:180) vs internal_stacks (unregistered, different template/signature, wrong layer). Registered Spec template never matches the internal_stacks output.
  • medium internal/daemon/job_executor_adapter.go:68; internal/modules/app/commands/system_install.go:57; internal/modules/hud/install_flow.go:147 — Three live forbidigo violations (golangci-lint exit=1): raw fmt.Errorf crossing a real module boundary. install_flow.go:147 is a huh-validation closure whose siblings (108/155) carry //nolint but 147 was missed.
  • medium internal/modules/cluster/commands/init.go:277,316,321; internal/modules/export/commands/commands.go:180,263,325,446,454,461; internal/modules/ctx/commands/logout.go:61,67 (177 files total) — 527 bare coded.New("X####...") literal call sites outside errors*.go construct coded errors inline, duplicating the template string where it can drift from the registered Spec (e.g. X400002VALINP hand-typed at ~30 sites).
  • medium X400010APPYML, X401000UNAUTH, X403027CLIPRV, X500430BKPEXP, X500900NOTWRD, X503999CMDNYI — 6 codes FULLY unregistered. (Re-verified 2026-07-31: 4 of the 6 now have factory funcs — X400010APPYML in modules/app, X401000UNAUTH in core/api, X403027CLIPRV in core/hostactivate, X500900NOTWRD in modules/network; X500430BKPEXP and X503999CMDNYI still have none. Spec-registration status not re-counted.)
  • medium internal/modules/app/errors_test.go:18-40; internal/core/api/manifest_test.go:21-39 — Only 12 of 61 module errors.go files have an errors_test.go, and those self-admit they “spot-check a handful”; there is NO global test asserting every factory code resolves via coded.Lookup. api/manifest_test.go uses stubProvider, not the real registry.
  • low tests/unit/core/coded/coded_test.go:24 (this repo) — X429000RATELT used as the 429-parsing example in the canonical test is a phantom: no factory, no Spec, no production call site. (Re-verified 2026-07-31 — still there.)

Lens 5 — logging convention (corelog vs raw slog)

Section titled “Lens 5 — logging convention (corelog vs raw slog)”

Partly RESOLVED. The three app/state_mirror.go slog.Warn items (202/209/224) are MOOT — state_mirror.go was deleted by ADR-0002 / B1 (reconciling with the “RESOLVED (ADR-0002 / B1)” banner in section (A)). The core/api/server.go stderr writes (264/349) are closed by fitness Check 11 daemon-stderr-ban (hard 0). The remaining plain-string .Warn(/.Error( items are now capped by fitness Check 13 untyped-logger (ratchet 30, falling) — they are tracked, not open-ended. Do not re-file the state_mirror items.

  • high internal/modules/app/state_mirror.go:202 — Package-level slog.Warn (the exact forbidigo-banned form, no nolint, no path-exclude); bypasses the typed API and ctx-handler enrichment. File is on a “leave untouched” list per the keycloak plan, so the violation persists.
  • high internal/modules/app/state_mirror.go:209 — Package-level slog.Warn for “stack row mirror failed” — banned form, no code, no fan-out; a DB mirror failure is invisible to the events surface.
  • high internal/modules/app/state_mirror.go:224 — Package-level slog.Warn for “stack row delete mirror failed” — same banned form, no code.
  • high internal/daemon/services_seams_health.go:202 — slog.Default().Warn(...) — the METHOD form via .Default() escapes the forbidigo ^slog\.Warn$ anchor. A leader-local-fallback degrade logged uncoded, contradicting the comment two lines above. Should be corelog.Warn(ctx, <coded>).
  • high internal/core/api/server.go:264 — fmt.Fprintf(os.Stderr, "shc: %s listener exited: %v") — library/daemon-layer code writing an error straight to stderr in a background goroutine, bypassing corelog (file imports neither core/log nor slog). Comment admits “For now stderr is the canonical place” — stale stopgap. Should be a coded corelog.Error.
  • high internal/core/api/server.go:349 — fmt.Fprintf(os.Stderr, "shc: uds listener exited: %v") — same library-layer stderr write for the UDS listener goroutine; uncoded, no corelog.
  • medium internal/modules/docker/events.go:275 — Raw logger.Error("docker events: permanent-fail threshold crossed", ...) — uncoded slog error; the SAME condition is then returned as coded X500066EVCMAX on the next line, so it is logged twice in two conventions.
  • medium internal/modules/docker/events.go:281 — Raw logger.Warn("docker events disconnect; will reconnect", ...) — uncoded, despite runErr being coded X500065EVCNCT just above.
  • medium internal/modules/docker/events.go:337 — Raw logger.Warn("docker event subscriber returned error", ...) — uncoded slog warn for a non-fatal subscriber failure.
  • medium internal/daemon/services.go:194 — Raw logger.Warn("user service skipped: nil db pool") — uncoded boot warn; errors.go defines a whole X503060QTANPL/X503061RPONPL/… family of “service skipped: nil db pool” coded warns for siblings, but the user-service skip was NOT migrated (no X503xxxUSRNPL). Inconsistent with the migration documented at internal/daemon/errors.go:5-8.
  • medium internal/daemon/services_extra.go:725 — Raw logger.Warn("deployment table backfill never converged") — uncoded; the adjacent hydrate failure DOES have X503072APPHYD.
  • medium internal/daemon/services_extra.go:1680 — Raw e.logger.Warn("unenroll: docker node rm failed", ...) — uncoded best-effort warn in the node-unenroll executor.
  • medium internal/daemon/services_extra.go:1698 — Raw e.logger.Warn("unenroll: deployment evict failed", ...) — uncoded.
  • medium internal/daemon/services_extra.go:1704 — Raw e.logger.Warn("unenroll: node repo delete failed", ...) — uncoded.
  • medium internal/core/config/watch.go:124 — Raw logger.Warn("config.Watch: failed to add directory", ...) — uncoded; the config-watch loop emits three uncoded warns (124/139/179).
  • medium internal/core/config/watch.go:139 — Raw logger.Warn("config.Watch: reload failed", ...) — uncoded.
  • medium internal/core/config/watch.go:179 — Raw logger.Warn("config.Watch: fsnotify error", ...) — uncoded.
  • medium internal/daemon/daemon.go:828 — Raw logger.Warn("daemon: DB-side config reload failed", ...) — uncoded; daemon owns X500994DMCNFG for config load but reuses none for the DB-side reload path.
  • medium internal/daemon/services.go:1 (shclog) vs daemon.go:1 (corelog) vs services_recovery.go (plain log) — Import-alias inconsistency for the SAME package internal/core/log: three names coexist — plain log (77), corelog (32), shclog (12). internal/daemon uses ALL THREE. The keycloak plan mandates corelog; shclog is a Python-era holdover.
  • low internal/modules/notification/dispatcher.go:360 — Raw log.FromCtx(ctx).Warn('notification rule disappeared' / 'rule invalid at send' / 'send failed') — three uncoded slog warns (360/365/370). Unlike the queue-full drop one line earlier (328, deliberate-exception + code X503010QUEFUL), these carry no code/rationale.
  • low internal/modules/internal_rpc/router.go:120 — Raw log.FromCtx(r.Context()).Warn("rpc handler failed", ...) — uncoded warn in the internal_rpc router (HTTP boundary); peers’ coded errors exist (X503073PEERHTT) but this path logs raw.
  • low internal/modules/forward/router.go:116 — Raw log.FromCtx(r.Context()).Warn("forward-auth: host resolve failed", ...) — uncoded.
  • low internal/cli/forward_wiring.go:230 — Raw log.FromCtx(ctx).Warn("forward: daemon ws dial failed", ...) — uncoded.
  • low internal/modules/shell/commands/exec.go:119 — Raw log.FromCtx(ctx).Error("exec failed", ...) (+ shell.go:116, inspect main.go:91, forward main.go:75) — four uncoded slog .Error calls in CLI surfaces using the plain .Error METHOD form that escapes forbidigo’s .ErrorContext ban. -> RESOLVED (2026-07-06) by the CLI double-emit fix (e22cda06c): all four .Error call sites were deleted — those surfaces now return the coded error to the top-level sink once instead of logging it AND returning it.
  • low internal/modules/storage/shared_manager.go:274 — Raw log.Default().Warn("storage detach failed", ...) — uncoded; the rest of shared_manager.go uses coded X200037MNTSKP/X200036LNKSKP, so this detach path is inconsistent with its own file.

Duplicate-route items RESOLVED (single-owner now). shc.stack.status is owned by the app module (stack cedes it — stack/router.go), shc.stack.move is owned by the clone module (stack cedes it), and the shc.backup.retention.* routes moved to the retention module (backup no longer registers them). The dead startAPIServer third flatten copy is also gone. The eager-vs-lazy resolver-layer notes below are style residue post the DI sweep.

  • high internal/modules/app/router.go:84 vs internal/modules/stack/router.go:74 — DUPLICATE ROUTE: GET /api/method/shc.stack.status registered by TWO modules (app svc-bound stackStatusHandler; stack resolver-bound handleStatus). Both blank-imported; the flatten loop iterates a Go MAP (non-deterministic) and PublicRouter().Method(...) silently overwrites — which handler serves is undefined per process start.
  • high internal/modules/clone/router.go:75 vs internal/modules/stack/router.go:78 — DUPLICATE ROUTE: POST /api/method/shc.stack.move registered by BOTH clone (moveStacksHandler) and stack (handleMove). Same non-deterministic last-writer-wins collision.
  • high internal/modules/backup/router.go:70-73 vs internal/modules/retention/router.go:98-101 — DUPLICATE ROUTE (x4): GET shc.backup.retention.get, POST shc.backup.retention.apply/set/reset all registered twice (backup retention*Handler vs retention handleGet/Apply/Set/Reset). Non-deterministic owner.
  • medium internal/modules/app/register.go:93 + internal/modules/app/router.go:70 — WRONG RESOLVER LAYER (dominant drift): the app router binds its *Service EAGERLY at build time (newRouter(d, GetRuntimeService())); handlers close over the captured svc. Captures nil if wiring order changes; ignores d.Services for its own Service.
  • medium internal/modules/backup/router.go:37 — WRONG RESOLVER LAYER: the backup router binds svc+sch eagerly at build time (newRouter(d, svc, sch)) rather than per-request resolve; same anti-pattern as app.
  • medium internal/modules/connections/router.go:35 — LEGACY SINGLETON: connections router resolves per-handler via the package-global GetRuntimeService() instead of the d.Services consumer interface; the builder takes _ coreregistry.Deps and discards Deps.
  • medium internal/modules/stack/router.go:66 (representative of ~29 routers) — CANONICAL IS THE MINORITY: only 7 of 36 registered routers type-assert d.Services to a consumer interface (dns, notification, user, auth, quota, export, backup/mount). ~29 either capture GetRuntimeService eagerly or ignore Deps. The “template every module router follows” (user/router.go:55) is followed by <20%.
  • medium internal/daemon/daemon.go:702-737 — DEAD DUPLICATE FLATTEN SITE: daemon.go:startAPIServer (672-744) contains a THIRD full copy of the registry.Routers()→chi.Walk→Method flatten (708-720) + duplicate RegisterManifestRoute/RegisterOpenAPIRoutes. Dead (//nolint:unused at 671) but still compiled — three copies of the flatten logic exist (daemon.go:702, unix_socket.go:135, internal_api.go:341).
  • medium internal/modules/connections/daemon_client.go:89, vault/http_client.go:93, shell/daemon_client.go:103, logs/daemon_client.go:78, ca/daemon_client.go:88, job/register.go:160 & :339, app/register.go:394, ctx/runtime.go:281, cli/forward_wiring.go:91, +11 more — DUPLICATE-IMPL (UDS dialer): 21 hand-copied DialContext unix closures re-implement what core/daemonclient.newHTTPClientAuth (client.go:153-167) already provides. core/daemonclient is imported by 17 files yet 21 sites still inline the dialer.
  • medium internal/modules/{connections,backup,ca,config,logs,shell}/daemon_client.go + {app,cluster,storage,task,health,node}/commands/daemon_client.go — DUPLICATE-IMPL (daemon-client wrappers): 12 daemon_client.go files each define a private wrapper type re-implementing socket-resolve + unix transport + non-2xx Frappe-envelope→coded.CodedError decode that core/daemonclient.Client already does.
  • low internal/modules/backup/router.go:79-80 — DUPLICATE ROUTE (intra-module, benign but inconsistent): backup registers shc.backup.schedule.list as BOTH GET and POST to the same scheduleListHandler — verb ambiguity not present on any sibling schedule route.

Lens 7 — state-persistence (Stack state)

Section titled “Lens 7 — state-persistence (Stack state)”

RESOLVED (2026-07-04) — ADR-0002 / B1 landed. Reconciles with the “RESOLVED (ADR-0002 / B1)” banner in section (A): state_mirror.go is deleted, app.State is demoted to the offline/test store (not a co-equal writer), and the app Service routes every stack read/write through one active deploymentStore (the DB deployment table). The mirror funcs, the perpetual disk-reconcile ticker, and the tombstone machinery are gone; ReconcileDiskStacks is a one-shot ImportOrphanManifests boot import. The duplicate stack.status/move routes are single-owner (see Lens 6). Every item below is closed — historical only. See NEXT-WAVES.md “State-model collapse (ADR-0002)”.

  • high internal/modules/app/state.go:191,259,302,335 (mirror calls) + internal/modules/app/state_mirror.go:175-227 — app.State is a CO-EQUAL WRITER to the deployment table, not a read-through cache. Every State mutator mirrors INTO the database (PutStack/UpdateStatus/SetDeployOutcome/DeleteStack), making the in-memory map an independent upstream writer — the inverse of the documented “repository delegates to State for hot reads” design.
  • high internal/modules/app/router.go:84 vs internal/modules/stack/router.go:74; collision at internal/daemon/daemon.go:702-720 — DUPLICATE stack.status route, two materially different handlers (repo→memory→disk fallback + docker enrichment vs StatusReader-delegated svc.Status); chi setEndpoint silently overwrites (last-write-wins over a non-deterministic map). (Cross-listed with Lens 6.)
  • high internal/modules/clone/router.go:75 vs internal/modules/stack/router.go:78 — DUPLICATE stack.move route, same non-deterministic-collision mechanism. (Cross-listed with Lens 6.)
  • high internal/modules/app/service.go:200,300,425 (path A) vs internal/modules/stack/service.go:393,450,738,768 (path B) — TWO independent install/status persistence paths write the deployment table with no shared funnel: Path A (app.Service→State.PutStack→mirrorStackRow→StackRepo.UpsertStack) vs Path B (stack.Service→recordStackRow/recordStackStatus→repo directly, bypassing app.State). The status_changed_at invariant is implemented TWICE; the paths can disagree.
  • high internal/daemon/services_seams_clone.go:257-306,332-339; services_recovery.go:207-214; services_clone_remote.go:217,349 — clone/move/recover WRITE-PATHS bypass the canonical authority: land a stack as state.json + containers only, then rely on the out-of-band ReconcileDiskStacks disk-sweep (boot + 5s ticker) and RegisterRecoveredStack to backfill app.State afterward. The clone GetStackFn comment admits the drift (“disk is the only truth this seam trusts”).
  • high internal/modules/app/state_mirror.go:1-227 (whole file) — state_mirror.go is the entire deletion-target bridge: ReconcileDiskStacks, RegisterRecoveredStack, MirrorAllToRepo, nodeIDResolver/mirrorStackRow*/mirrorStackDelete. The whole file exists only because app.State is a co-equal writer; collapsing State to a read-through cache removes the need for all of it.
  • medium internal/modules/app/state.go:24-26,246-274 + state_mirror.go:50-63 + state_tombstone_test.go — Tombstone machinery (deleted map + deletedAt + DeleteStack tombstone write) exists only to stop ReconcileDiskStacks from resurrecting a mid-teardown stack — a workaround for the disk-sweep backfill, which is itself a workaround for write-paths not writing the canonical store. Dead weight once the canonical write-path is enforced.
  • medium internal/modules/app/state_mirror.go:73-91 — ReconcileDiskStacks stamps StackStatusRunning + a guessed RuntimeCompose for any disk-only stack it backfills. It manufactures STATUS from the SPEC layer — a layering violation: observed status must come from docker/recorder, never invented by a disk reconcile.
  • medium internal/modules/app/service.go:460,473 (memory-only) vs router.go:278,338-339,963-964 (repo-first) — app.Service.GetStack/ListStacks read ONLY in-memory State (no read-through to StackRepo), while the HTTP handlers read repo-first. Two read layers with opposite authority ordering for the same data — a service-layer caller and an HTTP caller can see different truth.
  • medium internal/modules/app/router.go:350,379,981; services_seams_clone.go:279; services_seams_stack.go:703; services_extra.go:909 — readDiskStackFallback (disk→StackResponse enrichment) duplicated 4+ times: in the app HTTP router (two handlers), the clone seam GetStackFn, and the daemon status seams. Four+ hand-rolled copies of “read the state.json descriptor when DB/memory miss”.

RESOLVED (2026-07-04) — the global-swap sweep landed. di-test-fixtures is a hard 0 (.SetRuntimeService( in *_test.go = 0; the SetRuntimeService setters no longer exist), so the 327-site SWEEP BLOCKER and the parallel-plus-global-swap data-race items below are closed. (The dbtest_setup_test.go copy-paste and low-t.Parallel notes are style residue, not blockers.) Historical — do not re-fix the global-swap items.

  • high internal/modules/network/state_test.go:155 (+326 more sites in 64 files) — SWEEP BLOCKER: 327 test call-sites across 64 *_test.go files inject fakes by swapping a package global X.SetRuntimeService(fake) instead of constructing the service and passing through registry.Deps{Services:...}. This exercises only the LEGACY bridge; the canonical d.Services.(iface) path (23 production routers prefer it) is NEVER hit (0 tests set Deps.Services). Blocks deleting the 30 production SetRuntimeService setters (Stage-C cutover).
  • high internal/modules/network/tasks_test.go:18-160; network/state_test.go:147-235 — DATA-RACE: t.Parallel() mixed with package-global mutation in the SAME package without a serializing mutex. The cleanup write of runtimeSv races the parallel test’s read of GetRuntimeService. Latent only because -race is OFF in the default unit lane (Makefile:319 has no $(RACE)).
  • medium internal/modules/auth/auth_test.go:1047-1061 — WORKAROUND that should be the canon: auth_test.go invents a private runtimeMu/lockRuntime(t) mutex so its 30 t.Parallel tests opt out of the pool whenever they mutate SetRuntimeService. Correct local fix but a per-file reinvention — network/* did not copy it (hence the race). No shared testsupport.LockRuntime helper exists.
  • low repo-wide (t.Parallel: 22 files; t.Setenv: 196 sites) — Low t.Parallel adoption (22/752 = 2.9%). Structural: 196 t.Setenv() forbid t.Parallel, and the 64 global-swapping files cannot parallelize without the lockRuntime workaround. The two anti-patterns cap the parallel rate until the DI test migration lands.
  • low internal/modules/{audit,tenant,kv,node,quota,repo,retention,schedule,task,config,events,job,…}/dbtest_setup_test.go (16 copies) — dbtest_setup_test.go boilerplate copy-pasted into 16 module test packages — identical 8-line blank-import side-effect file differing only in the package X_test line. A shared internal test package or codegen check could collapse it.

Lens 9 — subsystem lifecycle + context ownership

Section titled “Lens 9 — subsystem lifecycle + context ownership”

RESOLVED (2026-06-22, LANDED) — fitness Check 7 now enforces it. The daemon-lifetime ctx is threaded through each loop subsystem’s DepsBuilder; the active backup_mount bug, the latent Start-ctx landmines, and the OnLeader-ctx captures (schedule_dispatcher / swarm_recorder / operation_registry / nebula rotation) were all rerooted, and genuinely-detached roots carry a // detached-ctx: marker. arch-fitness.sh subsystem-loop-start-ctx is a hard 0 (non-detached-ctx context.WithCancel(ctx) in subsystems/impl = 0). See NEXT-WAVES.md “Structural context ownership”. Historical — do not re-fix.

  • high internal/subsystems/impl/backup_mount.go:232 — ACTIVELY-CANCELLED Start-ctx capture: backup_mount.reapLoop derives its long-running goroutine ctx via context.WithCancel(ctx). backup_mount is priority 6 in a 3-member group, so its Start ctx is the gctx that startGroup cancels at group-startup completion (lifecycle.go:217) — the reapLoop ctx is cancelled right after boot. Latent only because the loop body is a no-op placeholder today.
  • high internal/modules/nebula/rotation.go:197 — Start-ctx capture across a module boundary: nebula RotationService.Start derives its rotation loop ctx from the caller ctx (context.WithCancel(ctx)), invoked as rs.Start(ctx) from cluster_cert_rotation.OnLeader. Safe-by-accident on the boot broadcast but structurally killable by any cancelled caller ctx. Should own context.Background().
  • high internal/subsystems/impl/schedule_dispatcher.go:96 — OnLeader-ctx capture: ScheduleDispatcher.startSweeper does context.WithCancel(parent) where parent is the OnLeader ctx. The sweepLoop dies if the leader-transition broadcast ctx is cancelled independent of a lose-leader edge. Should derive from context.Background().
  • high internal/subsystems/impl/swarm_recorder.go:143 — OnLeader-ctx capture: SwarmRecorder.startSweep does context.WithCancel(parent) (OnLeader ctx). Same class as schedule_dispatcher — long-running sweepLoop bound to the ephemeral leader-broadcast ctx.
  • high internal/subsystems/impl/operation_registry.go:89 — OnLeader-ctx capture: OperationRegistry.startPump spawns go s.pumpLoop(ctx, stop) with ctx = OnLeader arg. pumpLoop selects on ctx.Done() AND a stop channel; the ctx.Done() arm makes it killable by a cancelled leader-broadcast ctx. Should be Background-derived.
  • medium internal/subsystems/impl/docker_events.go:63 — Start-ctx capture (latent, single-member group): DockerEvents.run subscription ctx is context.WithCancel(ctx) from Start. docker_events is priority 9 alone → currently raw daemon-lifetime ctx (safe-by-accident); becomes the active footgun the moment any second priority-9 subsystem registers.
  • medium internal/subsystems/impl/container_recorder_node.go:111 — Start-ctx capture (latent, single-member group): ContainerRecorderNode tick/event loops use context.WithCancel(ctx) from Start. Priority 11 alone → currently safe; one co-priority subsystem turns it into the gctx-cancellation bug.
  • medium internal/subsystems/impl/collectors.go:319 — Start-ctx capture (latent, single-member group): Collectors.runTickLoop uses context.WithCancel(ctx) from Start. Priority -10 alone → currently safe, latent deviation from canonical Background ownership.
  • medium internal/subsystems/impl/collectors.go:136 — Dual-ordering (priority int vs deps list) divergence: collectors declares priority:-10 but deps:[migrations(5), container_runtime(-20)]; topoSort honours deps over priority, so it actually starts AFTER migrations. sse_broker and config_loader (priority 1) declare deps:[collectors], so the priority-1 bucket waits on a node whose -10 priority is decorative. Priority numbers lie about real start order for every negative-priority node depending on a positive-priority node.
  • low internal/subsystems/lifecycle/topo.go:107 — priorityGroups buckets purely by Priority with NO cross-dep splitting. The Subsystem interface doc claims “same-priority siblings with NO cross-deps start concurrently” — implying cross-deps prevent concurrency. They do not: two same-priority subsystems with a dep between them land adjacent after topoSort and start concurrently, racing the dep. Latent (no such pair today) but the grouping logic contradicts the documented contract.
  • high internal/modules/*/register.go (e.g. export BuildRouter, nebula:64 buildPublicRouter, auth newRouter) — Router-builder symbol has 6+ competing names for the identical role: buildRouter (14), NewRouter (10), inline anonymous func(d registry.Deps) (12), newRouter (auth), buildPublicRouter (nebula), BuildRouter exported (export). No single convention.
  • high internal/modules/metrics/register.go:95 (func() *Service) vs internal/modules/tenant/register.go:66 (*Service) — Same-named resolveService helper has TWO incompatible signatures: returns *Service in 7 modules (audit/logs/backup/network/schedule/tenant/config) vs func() *Service thunk in 7 (metrics/health/shell/task/events/cluster/vault). Identical name, divergent contract.
  • medium internal/modules/user/router.go:56 (userServices in router.go) vs internal/modules/tenant/register.go:51 (tenantServices in register.go) — The DI accessor interface + resolve helper live in register.go for 14 modules but in router.go for 7, splitting the wiring seam across two files. tenant/register.go:50 even tells readers to “see internal/modules/user/router.go” — pointing at the OTHER location.
  • medium internal/modules/user/router.go:73 (newRoutes/*routes) vs internal/modules/tenant/router.go:27 (newRouter free func) — Two router-construction shapes with no rule: a *routes{resolve} struct + newRoutes() builder (auth/dns/export/notification/quota/user) vs a free newRouter(deps, svc) func (tenant/app/audit/config/network/schedule/backup/ca/job/health/leader/metrics/task). Both claim to be “the DI template every module router follows”.
  • medium internal/modules/ca/store.go:112 (type Store), register.go:32 (GetRuntimeStore), daemon services.go:768 — ca is a full domain module but names its core type Store not Service, and uses GetRuntimeStore/SetRuntimeStore instead of canonical Get/SetRuntimeService. It has no state.go and no service.go.
  • medium internal/modules/backup/mount/register.go, internal/modules/health/reconcile/register.go — Nested sub-modules with their OWN register.go/router.go/service.go/state.go break the flat internal/modules// rule — registered modules buried inside a parent module dir.
  • medium internal/modules/operation/router.go:23, internal_stacks/router.go:40, sysroutes/router.go:35 — Three modules have init()+registry.Router registration but NO register.go file — the init() sits in router.go instead, violating B7’s “exactly one register.go”.
  • medium internal/modules/portforward/router.go (426 lines, no service.go), internal/modules/internal_rpc/router.go — portforward has a 426-line router.go (11 funcs) + proxy.go/swarm_resolver.go holding domain logic but NO service.go — handler and business logic merged in the HTTP layer (wrong-layer per B3:15-22). internal_rpc similar.
  • low internal/modules/forward/register.go (moduleName), leader/register.go:158 (literal), nebula/register.go:64 (literal) — ModuleName const casing drift: 44 modules use exported const ModuleName, but forward/inspect use unexported moduleName, and leader/nebula hardcode the string literal with no const.
  • low internal/modules/leader/register.go:103 (resolveRouterService), backup/register.go:64 (resolveRuntimeService), cluster/register.go:134 (resolveClusterService) — Four distinct second-resolver names for the same “get singleton” concept: backup (resolveRuntimeService+resolveService), task (resolveService+resolveRuntimeService), cluster (resolveClusterService+resolveService), leader (resolveService+resolveRouterService).
  • low internal/modules/hud/ (12 subdirs, no top-level router/service/state), internal/modules/storage/ (8 subdirs) — hud is a 12-subdir mega-module and storage is 8-subdir, far past the flat canonical shape; hud has no router.go/service.go/state.go at the top level (logic pushed into subdirs).
  • low internal/modules/ca/caapi, dns/dnsapi, proxy/proxyapi, storage/storageapi — The *api submodule naming and providers/ submodule appear in 4 modules with no documented convention; doctor uses diagnoses/, notification uses channels/, metrics uses collectors/, cluster uses noderepo/ — each module invents its own non-commands/ subdir name.

Lens 11 — duplicate-impl (daemon UDS client, scope parse, AdapterInstaller, runtime-service singleton)

Section titled “Lens 11 — duplicate-impl (daemon UDS client, scope parse, AdapterInstaller, runtime-service singleton)”
  • high internal/modules/vault/http_client.go:93 (+20 more) — Inline UDS dialer closure Transport{DialContext: func(ctx,_,_){ var d net.Dialer; return d.DialContext(ctx,"unix",socket) }} duplicated verbatim — 21 non-test, non-canonical copies of what daemonclient/client.go:158-161 already owns (ca, connections, shell, storage/commands, logs, job/register x2, app/register, app/commands, events/commands, task/commands, …).
  • high internal/core/daemonclient/client.go:51 (canonical) vs 17 dupes (connections:41, vault:40, ca:40, cluster/commands:33, shell:63, storage/commands:34, logs:38, task/commands:21, events/commands:383, health/commands:30, ctx/runtime:244, node/commands:38, job/register, app/register, backup:32, config:25, cli/forward_wiring:47) — daemonSocketCandidates = []string{"/run/shc/shc.sock","/var/run/shc/shc.sock"} re-declared in 18 files. Even modules that DID adopt daemonclient still re-declare it because daemonclient exposes no candidate list / EndpointConfigured() helper.
  • high node/commands/daemon_client.go:43 (SHC_URL-first) vs cluster/commands/daemon_client.go:38 (UDS-first) vs events/commands/main.go:388 (no runtime-dir) vs daemonclient/client.go:103 — resolveDaemonEndpoint() reimplemented 13x with FOUR semantically-divergent precedence orderings — a real correctness drift, not style: (A) daemonclient UDS→active-cluster→SHC_URL→loopback; (B) commands-template UDS→runtime-dir→well-known→SHC_URL; (C) node/commands SHC_URL FIRST; (D) events/commands OMITS the SHC_RUNTIME_DIR probe.
  • high internal/modules/ca/daemon_client.go:33-154 vs connections/daemon_client.go:33-155 vs vault/http_client.go:30-160 — Full daemon-client wrapper (struct + do/get/post + method-envelope decode) duplicated “field for field” (their own comment) across ca and connections — line-for-line copies differing only in the X-error-code identifier and type name. vault is the cited “original”.
  • medium daemonclient/client.go:129 (canonical IsUDSSocket) vs vault:72, ctx/runtime:268, cluster/commands:60, shell:90, storage/commands:61, logs:65, task/commands:48, health/commands:60, node/commands:68, events/commands:404, ca:68, connections:69 — isUDSSocket(p) (os.Stat ModeSocket check) reimplemented under SEVEN different names for byte-identical bodies: IsUDSSocket, isUDSSocket, isSocket, isUnixSocket, isCAUDSSocket, isConnectionsUDSSocket.
  • medium daemonclient/client.go:163 (30min) vs vault:98 (120s), ca:93 (120s), health/commands:80 (10s), node/commands:86 (30s) — Per-module daemon-client HTTP Timeout drifted across copies of the SAME client: 30min (canonical), 120s (vault/ca/connections/shell), 30s (node/cluster/task/storage/app/logs), 10s (health). Streaming endpoints behave differently depending on which copy the CLI surface calls.
  • medium internal/modules/config/types.go:33 scopeMap vs vault/register.go:390 secretScopeFromFilter vs vault/service.go:873 scopeFilterColumn vs hud/config_view.go:75 — Scope input-synonym parse table hand-rolled 4+ times because internal/core/scope has no Parse(). Each accepts a DIFFERENT synonym set: config (base/global + env, no ‘system’); vault register (global/base/system, no ‘env’); vault service (global/base/system); hud (environment/env).
  • medium internal/modules/config/state.go:21-47 vs backup/state.go:41-73 — AdapterInstaller race-closer (type AdapterInstaller func(*Service); SetAdapterInstaller; fire-installer-inside-SetRuntimeService) implemented TWICE, verbatim — config and backup. Same mechanism, same comment, copied not shared.
  • low internal/modules/config/state.go:15-57 (cleanest) vs 29 other declarations (vault/audit/schedule/tenant/task/health/job/network/…) — runtimeMu/runtimeSv + Set/GetRuntimeService singleton trio copy-pasted into ~30 module state files with no shared base type or generic. The codebase’s most-replicated pattern (41 files reference runtimeSv).

Lens 12 — interfaces-seams (consumer-defined-interface / dependency inversion)

Section titled “Lens 12 — interfaces-seams (consumer-defined-interface / dependency inversion)”
  • high internal/modules/stack/router.go:42, clone/router.go:37, storage/router.go:34, retention/router.go:66, repo/service.go:791, search/router.go:51, node/router.go:85 — Seam-shape split: 21 modules use the canonical type xServices interface { XService() *Service } read through registry.Deps.Services, but 6-7 modules declare NO consumer interface and never read d.Services — they resolve only through a legacy package-level SetServiceResolver(func...) singleton. Two parallel ways to do the identical “router gets its Service” job.
  • medium internal/modules/clone/router.go:72, internal/daemon/services_seams_clone.go:230, internal/daemon/composition.go:946 — clone is documented as a fully-converted W3 root module, but the conversion is producer-side ONLY: clone’s router ignores Deps (buildRouter(_ registry.Deps)) and resolves via the package currentResolver() singleton, never CloneService(). The sole reader of the root’s CloneService() is the daemon reading its OWN field — no consumer-read seam exists for clone.
  • medium internal/modules/job/deployment_vars.go:54+61, metrics/rpc_utility.go:32+39, connections/creds.go:57+64, ca/store.go:786+795 — Duplicate-implementation: FOUR modules independently hand-roll the same “consume vault via a concrete-returning resolver func” seam (var vaultResolver = vault.GetRuntimeService + SetVaultResolver(func() *vault.Service)). Each returns the CONCRETE *vault.Service, so they import the whole vault package for one or two methods.
  • medium internal/modules/job/deployment_vars.go:61, connections/creds.go:64, metrics/rpc_utility.go:59 — Wave-2 inconsistency / wrong-layer abstraction: of the three modules with one vault edge, only metrics inverts correctly (narrow consumer interface vaultStatusProvider); job and connections take func() *vault.Service returning the concrete type. The DI plan promised consumer interfaces connections.VaultService and job.VaultResolver; neither was built. The proper inversion exists right next to the violation.
  • medium internal/daemon/composition.go:54 (Services has no var _ = ... block), internal/modules/forward/interfaces.go:37 (the pattern that should be mirrored) — No compile-time satisfaction guard: the 21 consumer xServices interfaces are bound to *daemon.Services ONLY by runtime d.Services.(xServices) assertions. There is NOT ONE var _ scheduleServices = (*Services)(nil). If a *Services accessor signature drifts/renames, the assertion silently fails at runtime and falls back to the legacy singleton — the opposite of forward’s gold-standard var _ ForwardResolver = (*Service)(nil).
  • low internal/modules/app/dns_target.go:97, app/inject_traefik.go:922, app/inject_traefik.go:1024 — Source-seam shape inconsistency: proxy/ca/dns declare NAMED xSource interface types (ProxyCredSource, CASignerCredSource, ConnectionCredSource), but app declares its three Source seams as inline ANONYMOUS func types (SetProxyVIPSource, SetSystemTraefikDNS01Source, SetSystemTraefikProxyProtocolSource). Same DI intent, two shapes; the anon-func form has no named contract to assert against.
  • low internal/modules/node/router.go:85 — node’s consumer seam uses a bare multi-return func func(ctx) (Store, HealthChecker, SwarmTokenProvider, error) instead of an xServices interface or a single Source interface — an outlier shape no other module uses, bundling three unrelated capabilities into one positional-tuple resolver.

scripts/arch-fitness.sh is the architecture guard. The rules it already enforces are listed first; the remaining proposed fitness_rules from the 12 lenses are PENDING, each with the greppable check that should be added.

Already ENFORCED by scripts/arch-fitness.sh

Section titled “Already ENFORCED by scripts/arch-fitness.sh”

CROSS-CHECKED against the live script (2026-07-04). The roster below now matches the ~20 checks actually in scripts/arch-fitness.sh (the earlier 6-item list was stale both ways: it cited deleted baselines for di-singletons/uds-dialer-copies, and ~14 checks — several of them PENDING proposals below that have since been wired — were unlisted).

  • di-singletons (ratchet, HARD 0) — the *.SetRuntimeService package globals are fully deleted; a new one fails make lint. (Was “pins the 29-bridge baseline” — WRONG: the bridge is gone, the count is a hard zero. Lens 3 rule #1.)
  • di-runtime-families (ratchet 8, falling) — the broader func SetRuntime* family ceiling; each sweep lowers it (fell 9→8 after the stack.RuntimeState sweep). See NEXT-WAVES.md “Remaining Runtime-singleton families”.
  • one-scope-type (hard) — the scope ladder stays the single core/scope.Scope {Global,Tenant,Environment,Stack,Deployment} type (no resurrection of types.ScopeTenant/ScopeLocal/the dead 6th tier).
  • no-cgo-sqlite (hard) — no mattn/go-sqlite3 import in internal//cmd/; only pure-Go modernc.org/sqlite (driver “sqlite”), keeping the hermetic CGO_ENABLED=0 build buildable (Lens 8 rule #3).
  • layer-boundary (hard) — no provider imports a consumer (impl never imports daemon; no internal/modules/* non-test file imports internal/daemon) (Lens 12 rule A).
  • core-imports-modules (hard) — internal/core must not import internal/modules (the reqctx-extraction guard; core→modules would re-manufacture the log/config/api import cycles). ADR-0006.
  • di-test-fixtures (ratchet, HARD 0) — the X.SetRuntimeService(fake) global-swap test pattern is gone (.SetRuntimeService( in *_test.go = 0). Lens 8 rule #1.
  • uds-dialer-copies (ratchet, HARD 0) — inline d.DialContext(ctx, "unix", ...) closures outside internal/core/daemonclient/ are at 0 (was “baseline 21” — WRONG: the dedup landed, ceiling is a hard zero). Lens 6 / Lens 11 rule #1.
  • subsystem-loop-start-ctx (hard) — no long-running subsystems/impl loop derives from the Start ctx; genuinely-detached roots carry a // detached-ctx: marker. This is the wired form of the Lens 9 NO-START-CTX-CAPTURE proposal below.
  • error-template-style (hard) — coded-error template text is lowercase, no colons/semicolons/parens/trailing-period. Issue #105.
  • xcode-numeric-collision (ratchet 138, falling) — a NEW code may not reuse an existing X-number. (Lens 4 family.)
  • inline-coded-new (ratchet 222, falling) — coded.New("X…") at a call site (outside commands/ and the errors*.go factory homes) must not grow; mint via the registered factory. Wires part of the Lens 4 error-handling proposals below. Issue #105.
  • no-local-output-flag (ratchet 0) — no command re-registers --output/-o/--json/--format; the root owns it. Output contract §2.
  • daemon-stderr-ban (ratchet 0) — no fmt.Fprint*(os.Stderr, …) in internal/daemon / internal/subsystems / internal/core/api. This is the wired form of the Lens 5 ESCAPE-HATCH stderr-write proposal below. Output contract §12.
  • key-value-output-ban (ratchet 0) — no key=value lines printed to humans in */commands. Output contract §4.
  • untyped-logger (ratchet 30, falling) — plain-string .Warn("…") / .Error("…") logger calls capped; operator-actionable logs carry an X-code via typed corelog. This is the wired form of the Lens 5 ESCAPE-HATCH .Warn("…")-method proposal below. Output contract §12.
  • example-floor (FLOOR 20, may only grow) — cobra Example: blocks are contract and must not be dropped in a refactor. Output contract §10.
  • cli-daemon-routing (hard) — every module that registers CLI commands dials the daemon through core/daemonclient (unless allowlisted).
  • app-image-float (ratchet 3) — concrete apps/ image refs must pin a tag; 3 grandfathered upstream-constrained refs.
  • no-connection-in-render-ctx (hard) — a connection object must never become a jinja key in the app render context (cross-tenant cred-leak guard).

PENDING — proposed fitness rules not yet wired

Section titled “PENDING — proposed fitness rules not yet wired”

Several PENDING proposals below have since been WIRED (annotated → WIRED inline): Lens 9 NO-START-CTX-CAPTURE (as subsystem-loop-start-ctx), Lens 5 ESCAPE-HATCH-GREP (split into daemon-stderr-ban + untyped-logger), and the Lens 4 coded-error hygiene proposals (partly, as inline-coded-new + error-template-style + xcode-numeric-collision). The Lens 3 write-only-accessor / Lens 6 duplicate-route drift the proposals targeted is also CLOSED by the DI sweep + B1 (the rules themselves were never added because the drift is gone). The rest below are genuinely still pending.

Lens 1 — repo-docs

  • DOC-PATH-EXISTS: grep -rnE 'internal/daemon_client|internal/core/(types|errors)\b|internal/models\b|internal/subsystems/(events|queue|sse|docker|database)\b|internal/(log|shell)\b' docs/ README.md must exit non-zero on a hit (all known-dead paths).
  • STALE-API: grep -rnE '\bgorm\b|types\.Scope(Tenant|Local|Deployment)|config\.(GetScoped|GetResolved|Scope)\(|\.NewCloneService\(|\.NewNodeService\(|app\.DeploymentModel|database\.GetDB\(' docs/ README.md — any match = a doc claim that no longer matches HEAD.
  • BROKEN-LINKS: for each markdown relative .md link in docs/ + README.md, test -f <resolved> must succeed (catches README.md:172 backend/models.md, backend/queries.md).
  • DI-PLAN-RESTATUS: fail if di-migration-plan.md still says ‘exactly five edges’ / ‘AdapterInstaller (deleted)’ while grep -rc 'SetRuntimeService' internal/daemon/composition.go > 0 (bridge still live).

Lens 2 — godoc-comments

  • DANGLING-SYMBOL: git grep -n 'scopeToSecretScope' -- '*.go' ':!*_test.go' must return ONLY comment lines if the identifier has no definition (a comment naming a non-existent non-test symbol fails).
  • STALE-MIGRATION-PROSE: git grep -n 'More land here\|as each wave converts\|wave 1 — leaves' -- internal/daemon/composition.go must be empty once W3 is the last wave.
  • DEPSBUILDER-EXAMPLE: assert no comment claims bootstrap takes a nil deps argument while bootstrap registers a DepsBuilder — f=internal/subsystems/impl/bootstrap.go; grep -q 'DepsBuilder:' "$f" && git grep -q 'self-contained subsystems like bootstrap' -- internal/subsystems/lifecycle/registry.go && echo FAIL.
  • SCOPE-COMPLETENESS: any field-doc on a struct with an Environment column that enumerates scope tiers must mention ‘Environment’ and ‘Deployment’ — git grep -lz 'Environment ' -- '*models.go' | xargs -0 -I{} sh -c 'grep -q "stack-scoped row" {} && ! grep -q "Deployment" {} && echo STALE: {}'.

Lens 3 — di-composition (rule #1 ENFORCED as di-singletons; remainder pending)

  • NO-WRITE-ONLY-ACCESSOR: for each func (s *Services) XService() in composition.go, require a matching consumer-side interface method declared in internal/modules — for acc in DockerService CloneService; do grep -rqn "${acc}()" internal/modules/ || echo "WRITE-ONLY ACCESSOR: $acc"; done (fails today on DockerService/CloneService/ForwardService).
  • W2-PER-CALL-CLOSURE: ! grep -rnE ':=\s*d\.services\.(Vault|Task|Schedule)Service\(\)' internal/daemon/ | grep -v 'if svc :=' (a capture assigned then passed onward is the bug; legitimate uses are guarded if svc := ... inside a closure body).
  • ONE-ANY-RESOLVER (lint companion): test $(grep -cE '\bany\b' internal/subsystems/lifecycle/registry.go internal/subsystems/lifecycle/lifecycle.go | paste -sd+ | bc) -le 4 (a new any Resolver field fails CI).

Lens 4 — error-handling (→ WIRED in part: inline-coded-new, error-template-style, and xcode-numeric-collision now enforce coded-error hygiene; FORBIDIGO-BLOCKING itself still pending)

  • FORBIDIGO-BLOCKING: wire golangci-lint run to fail the build (it already exits 1 on the 3 boundary fmt.Errorf sites).
  • EVERY-FACTORY-REGISTERED: comm -23 <(grep -rhoE '^func (X[0-9]{6}[A-Z]{6,})' internal | awk '{print $2}' | sort -u) <(grep -rhoE 'Code: *"X[0-9]{6}[A-Z]{6,}"' internal | grep -oE 'X[0-9]{6}[A-Z]{6,}' | sort -u) MUST be empty.
  • NO-DUPLICATE-FACTORY: grep -rhoE '^func (X[0-9]{6}[A-Z]{6,})' internal | awk '{print $2}' | sort | uniq -d MUST be empty (catches X200003OPFAIL, X500071DEPFAL).
  • REGISTRY-INTROSPECTION test: a Go test in internal/core/coded that ranges the live registry and asserts every referenced X-code resolves via coded.Lookup AND the registered Spec.Template matches the factory’s rendered template.

Lens 5 — logging convention (→ WIRED in part: daemon-stderr-ban (hard 0) covers the library-layer stderr writes and untyped-logger (ratchet 30) caps the .Warn("…") method form; the slog.Default() forbidigo rule + IMPORT-ALIAS gate are still pending)

  • ESCAPE-HATCH-GREP → WIRED as daemon-stderr-ban + untyped-logger (see the ENFORCED roster above). grep -rnE 'slog\.Default\(\)\.(Warn|Error)\(|\.(Warn|Error)\("|fmt\.Fprint(f|ln)?\(os\.Std(err|out)' … — the stderr and .Warn("…") arms are now guarded; the slog.Default() arm remains a pending forbidigo rule.
  • forbidigo rule pattern: '^slog\.Default$' to kill the .Default() bypass.
  • IMPORT-ALIAS gate: every import of internal/core/log must be aliased corelog (forbid bare log and shclog).

Lens 6 — http-routing (UDS-dialer rule ENFORCED as uds-dialer-copies; the duplicate-route DRIFT the rules below target is now CLOSED — routes are single-owner — though the guard rules themselves were not added)

  • NO-DUPLICATE-ROUTE: extract every chi-verb route literal from non-test internal/modules/**/{router,register}.go and fail if any route appears in 2+ distinct module dirs — ... | sort | uniq -d MUST be empty. (The three families it flagged — shc.stack.status, shc.stack.move, shc.backup.retention.{get,apply,set,reset} — are now single-owner, so the check would pass today; wire it to prevent regression.)
  • ONE-FLATTEN-SITE: grep -rl 'for name, build := range registry.Routers()' --include='*.go' internal | grep -v _test MUST return only the subsystems/impl files (fail if internal/daemon/daemon.go still appears → kill dead startAPIServer).

Lens 7 — state-persistence

  • NO-DUPLICATE-API-ROUTE: grep ... 'r.(Get|Post|Put|Delete)("/api/..." then sort | uniq -d must be empty (flags /api/method/shc.stack.status app-vs-stack and shc.stack.move clone-vs-stack).
  • NO-NEW-MIRROR-WRITER: git grep -nE 'mirrorStackRow|mirrorStackDelete|ReconcileDiskStacks|RegisterRecoveredStack|MirrorAllToRepo' -- internal/modules/app/state_mirror.go internal/modules/app/state.go | wc -l must be monotonically NON-INCREASING; end-state git grep -n 'mirrorStackRow\|state_mirror.go' internal/modules/app/state.go must be EMPTY (StackRepo.UpsertStack is the sole stack-row writer).

Lens 8 — testing conventions (di-test-fixtures + no-cgo-sqlite ENFORCED; remainder pending)

  • NO-PARALLEL-PLUS-GLOBAL-SWAP: for each t.Parallel test file that also calls SetRuntimeService and lacks lockRuntime/runtimeMu, fail (fires today on network/{state,tasks}_test.go).
  • HERMETIC-LANE-BUILDS (optional): CGO_ENABLED=0 go build ./internal/... ./cmd/... must stay green.

Lens 9 — subsystem lifecycle (NO-START-CTX-CAPTURE → WIRED as subsystem-loop-start-ctx, hard 0)

  • NO-START-CTX-CAPTURE → WIRED as fitness Check 7 subsystem-loop-start-ctx (hard 0): grep -rn 'context.WithCancel(ctx)' internal/subsystems/impl --include='*.go' | grep -v _test.go | grep -v 'detached-ctx:' is empty. All the sites it listed (docker_events, backup_mount, container_recorder_node, collectors, schedule_dispatcher, swarm_recorder, nebula/rotation) were rerooted onto the daemon-lifetime ctx.
  • NO-STALE-ERRGROUP-DOC: ! grep -q 'errgroup' internal/subsystems/lifecycle/*.go || grep -q 'golang.org/x/sync/errgroup' internal/subsystems/lifecycle/*.go.
  • CATALOG-SIZE test (optional): assert CatalogSize == len(register.go .Registration() entries) and that no two same-priority registrations declare a dep on each other.

Lens 10 — module-structure

  • ROUTER-BUILDER-NAME: grep ... registry.Router(...) builder symbol MUST yield only buildRouter (any NewRouter/newRouter/BuildRouter/buildPublicRouter or inline anon-func literal fails).
  • RESOLVE-SIGNATURE: grep -rh 'func resolveService(d registry.Deps)' internal/modules/*/register.go | sort -u MUST yield exactly ONE signature line (today: *Service and func() *Service).
  • ONE-REGISTER.GO: flag any module dir that calls registry.Router/Commands from router.go without a register.go (operation/internal_stacks/sysroutes).
  • FLAT-LAYOUT: find internal/modules -mindepth 2 -name register.go ! -path '*/commands/*' MUST be empty (flags backup/mount, health/reconcile, hud/server, backup/mount/subproc).
  • MODULENAME-CONST: grep -L 'const ModuleName' internal/modules/*/register.go and grep -rn 'registry.Router("' internal/modules/*/register.go MUST both be empty (flags forward/inspect moduleName + leader/nebula string literals).

Lens 11 — duplicate-impl

  • NO-INLINE-UDS-DIALER → SATISFIED: grep -rn 'd.DialContext(ctx, "unix"' --include='*.go' internal/ | grep -v '_test.go' | grep -v 'internal/core/daemonclient/' is now empty (the dedup landed; the ENFORCED uds-dialer-copies ratchet is a hard 0, not the old baseline 21).
  • NO-DUP-SOCKET-CANDIDATES: grep -rln '"/run/shc/shc.sock",' --include='*.go' internal/ | grep -v '_test.go' | grep -v 'internal/core/daemonclient/client.go' MUST be empty (baseline 17).
  • NO-PRIVATE-ENDPOINT-RESOLVER: grep -rn 'func resolve.*DaemonEndpoint\|func resolveEndpoint' --include='*.go' internal/ | grep -v 'internal/core/daemonclient/' MUST be empty (baseline 13).
  • NO-SOCKET-STAT-SYNONYMS: grep -rnE 'func (is|Is)(Unix|UDS|CAUDS|ConnectionsUDS)?Socket' --include='*.go' internal/ | grep -v 'internal/core/daemonclient/' MUST be empty.
  • NO-SCOPE-SYNONYM-SWITCH: once a shared scope.Parse exists, grep -rn 'case "base", "global"\|case "global", "base", "system"\|case "environment", "env"' --include='*.go' internal/ | grep -v 'internal/core/scope/' MUST be empty (baseline 4 sites).

Lens 12 — interfaces-seams (rule A ENFORCED as layer-boundary; remainder pending)

  • NO-NEW-LEGACY-RESOLVER: fail if a module declares func SetServiceResolver AND lacks a type [a-z]*Services interface in the same package — should list only the 6-7 known stragglers (stack/clone/storage/retention/repo/search/node) and never grow.
  • NO-CONCRETE-CROSS-MODULE-RESOLVER: grep -rn 'func() \*(vault\|dns\|docker\|configmod)\.Service' internal/modules --include='*.go' | grep -v _test pinned at the current count (9 across job/metrics/connections/ca/app); a new one fails CI, forcing a consumer-defined interface.
  • SATISFACTION-ASSERTION (positive guard): assert var _ <xServices> = (*Services)(nil) exists in internal/daemon for every type [a-z]*Services interface in internal/modules, so accessor drift becomes a compile error not a silent runtime fallback.

(D) Carried over from prior handoffs (HANDOFF.md + handoff2.md)

Section titled “(D) Carried over from prior handoffs (HANDOFF.md + handoff2.md)”

Both HANDOFF.md and handoff2.md were reconciled against HEAD (2026-06-22). Their DI sections are SUPERSEDED — the “W2/W3 cross-module edges still open” warnings are STALE (this session converted those edges) and the Stage-C cutover they describe is the legacy sweep already tracked by the di-singletons ratchet above. All 6 of HANDOFF.md’s cluster-confirmed fixes verified INTACT (no regression from the DI/scope refactors). Only these items are genuinely open:

  • Phase B — launcher injection. RESOLVED (#106): the launcher is pure scoped config (launcher key, deployment > stack > environment > tenant > global, then the app.yaml launcher: default) — the launcher *bool state hybrid (models/types/repository/CLI --launcher/--no-launcher) was deleted and the deployment.launcher column retired in place (migrations frozen). Injection rides the shc-oidc gating seam: writeIngressRouteFile chains + defines a per-deployment shc-launcher-<project> rewritebody middleware in the file-provider route file (NOT docker labels — cross-node safe under exposedByDefault: false); MaterializeIngress re-resolves per sweep via launcherForDeployment. Deployment-scoped launcher config rows already ride BackupManifest.DeploymentConfig generically.
  • shc cron ls operator surface. RESOLVED: internal/modules/cron is the read-only view over registry.CronsAll() — GET /api/method/shc.cron.list (daemon) + shc cron ls (CLI, local-or-daemon adapter) rendering Name/Module/Schedule/Class/Timeout/LeaderGated plus the schedule-derived next fire, through the same crons.<name> override resolution the cron_runner applies (the registry records no fire history, so there is no last-run column).
  • Dead events.OnCron / PendingCrons / CronRegistration seam. -> (RESOLVED: the seam is gone — no OnCron/PendingCrons symbols remain in modules/events/handlers.go at HEAD (deleted with the cron-system rework that made CronRunner the sole CronsAll consumer). Verified 2026-07-31.)
  • docs/proposals/config-v2.yaml is stale. Its timeouts/api.timeout (66-75,68), keycloak (99-107), and otel (36) sections describe shapes that shipped differently — esp. it still describes the ABANDONED keycloak stack→URL resolver, which would mislead a future agent into rebuilding the two-address split the SHC_HOST-derived model removed. Rewrite or banner it (like di-migration-plan.md).
  • config.go:299 SHC_KEYCLOAK_URL → keycloak.url dead env alias. -> (RESOLVED: no SHC_KEYCLOAK_URL reference remains in core/config/ at HEAD. Verified 2026-07-31.)
  • Phase H — per-deployment scoping breadth (ongoing, low priority). Only ~3 app-layer keys (dns.public_address/dns.ttl/dns.proxied) use the scoped Service.Get(ctx,key,Ctx{…}) walk (proxy.connection was retired by proxy-plane v2 — LB attachment lives in --port <conn>@… mappings now); every other cfg.String/Int reader uses the ambient global tree. The architecture is settled (no hybrids); migrating a consumer to scoped reads is per-key judgment, not a bug.
  • DNS dns.target comment drift (low). -> (RESOLVED: modules/app/dns_target.go now opens with “there is NO scoped dns.target config tier” and documents the per-deployment precedence ladder; no stale scoped-tier prose remains. Verified 2026-07-31.)
  • Infra (not code): 8 GB swap / ceph headroom for a 100%-green e2e. ensure-swap.sh defaults SWAP_GB=4; bump to 8 and/or cap ceph mon/osd memory before chasing a fully-green proxmox run. Run ensure-swap.sh --all after every provision (deliberately NOT wired into make). Likely the same OOM root cause as #88 (node crash → “database is locked” on the old embedded-raft engine → lost enroll write).
  • Process note (not in-tree): fresh-base DI fan-out. Any future parallel worktree-judge DI run must base candidates on live main HEAD (an old run forked from a stale base predating the DI spine → empty no-op diffs); the judge should git apply --check against HEAD and reject out-of-module edits.
  • node table: 17 write-orphan/read-orphan columns + a scheduler NULL-scan defect (catalogued 2026-07-17 by the sweep-legacy codes-config group; no schema change made). 17 node-table columns are never written AND never read by any non-test code path: labels, architecture, os, state_dir, runtime_dir, config_dir, http_address, last_seen_at, joined_at, node_metadata, version, swarm_status, swarm_manager_addr, health_status, health_checked_at, default_storage_provider, deploy_max_parallel — the only writers are UpsertNode’s raw SQL (16 named columns) and the narrow cluster_state/availability/mesh_ip/public_address/swarm-identity update paths; the only read projection is entNodeToTarget (13 columns). Two more (role, resources) are never written but ARE selected: scheduling_source.go:161-164 SELECTs them into plain strings, so the NULL columns fail the Scan and the continue skips EVERY row — the metrics node-schedule source is structurally EMPTY in production. Fix the scheduler to stop selecting never-written columns (or scan through sql.Null*) regardless of the wire-or-drop outcome. Related half-wire: availability IS durably written (drain’s SetAvailability → UpdateAvailability) but never read back — entNodeToTarget doesn’t project it, ClusterNodeResponse.Metadata["availability"] stays nil (so drain’s idempotence gate X409400CLSDRD and repair’s LastSeenAt staleness leg are structurally dead — see the field comment in cluster/types.go), and the node resource API hardcodes Availability: "active". Per-column wire-or-drop is a plan-level decision: drops need an atlas node-table rebuild + touches to entNodeToTarget, the CLI render list (node/commands/stubs.go nodeDoc), the REST response shape (node/types.go), and ent codegen; health_status/last_seen_at look like un-landed feature slots (heartbeat), not junk.
  • ent node.cluster_state CHECK annotation half-landed (catalogued 2026-07-17; no schema change made). The CHECK annotation (including fence_pending) sits in ent/schema/node.go:55-70 with its own OWED comment, but go generate ./ent + the migration never ran: the generated NodeTable.Annotation carries NO Check (ent/migrate/schema.go — sibling tables’ checks ARE generated: Deployment/JobRun/Task), and no migrations/*.sql carries cluster_state IN (the 2026-07-16 drop_node_dqlite_id node-table rebuild re-creates the column unconstrained). Enforcement today is Go-level only — validClusterStates/IsValidClusterState gate BOTH the ent and raw-SQL write paths, so no out-of-set rows can exist and materializing the CHECK later is data-safe. Hazard: any unrelated make migrate (in the ce repo, which owns ent/ and migrations/) after an ent regen will surprise-materialize a node-table rebuild. Decide as a discrete commit: run go generate ./ent + make migrate NAME=node_fence_pending_state (both in ce), or delete the annotation and document the Go gate as the single enforcement point.