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 aninternal/tree. Resolve audit-time paths per the repo convention (feature-graph.yaml header): strip theinternal/prefix — baremodules/,core/,daemon/,subsystems/,ent/paths live in the pinned ce module;shc/...-prefixed paths are this repo, root-relative (the audit-eraee/...prefix and_eefile suffixes died in the 2026-07-31 ee/ flatten — read an oldee/Xas today’sshc/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.mdis 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.mdwas rewritten to defer to the contract,docs/reference/errors.mdwas 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 jsonenvelope), 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.
docs/proposals/di-migration-plan.md
Section titled “docs/proposals/di-migration-plan.md”CLOSED (2026-07-04) — the DI sweep COMPLETED; every item below is moot. The Stage-C cutover landed:
di-singletonsis a hard 0 (func (Set|Get)RuntimeServiceover non-test Go returns 0), the dual-write bridge is deleted, anddi-migration-plan.mdnow 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.)
docs/architecture/patterns/import-dag.md
Section titled “docs/architecture/patterns/import-dag.md”DONE (2026-06-24). This doc was rewritten in commit
ba09a0285; both items below are FIXED. It now states there is no GORM and nointernal/models, referencesentthroughout, and the layer map matches the realinternal/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-argevents.Registerexists — registration usesevents.DispatchSinglewith anevents.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 usesinternal/core/daemonclient, saysapp.StackModel (formerly DeploymentModel), and explicitly notes “there is nodatabase.GetDB” and “There is noapp.HandleMoveStacks”. Do not re-fix.
- cross-module.md:18-22,110,200 — Commands import
shc/internal/daemon_clientand calldaemon_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-162 —
BackupService.CreateForStackuses GORM (s.db.Where(...).First(&stack).Error,gorm.ErrRecordNotFound) over anapp.DeploymentModel; tables referenceapp.DeploymentModel-> ORM is ent, not GORM (nogorm.ErrRecordNotFoundpath exists), and the typeapp.DeploymentModelwas renamed toapp.StackModel(internal/modules/app/models.go:50). “DeploymentModel” now appears only in code comments, never as a live type. - cross-module.md:86,121,192 —
clone.NewCloneService(ctx, db);node.NewNodeService(ctx, db);database.GetDB()-> Real constructors areclone.NewService(p Providers, tenant TenantResolver)(clone/service.go:62) andnode.NewService(target NodeTarget, opts ...ServiceOption)(node/service.go:124). There is nodatabase.GetDB()function in the tree. - cross-module.md:78 —
app/router.godefinesfunc HandleMoveStacks(w, r)importingclone.NewCloneServiceforshc.stack.move-> NoHandleMoveStackssymbol exists in internal/modules/app. The example handler name and body are stale; locate the current move handler before re-documenting.
docs/architecture/modules/config.md
Section titled “docs/architecture/modules/config.md”DONE (2026-06-24). This doc was rewritten in commit
ba09a0285; all three items below are FIXED. It now states “There is no package-levelconfig.Get/GetScoped/Scope, and notypes.ScopeTenant/types.ScopeLocal” and documents the five-tiercore/scopeladder. 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 ofconfig.Get/GetScoped/GetResolved/Scopeexist at HEAD, andtypes.ScopeTenant/types.ScopeLocalwere removed by the scope collapse. The scope type isscope.Scope {Global,Tenant,Environment,Stack,Deployment}(scope/scope.go:25-39) with no “Local” tier; the real entry points are config Service methodsGet(ctx,path,sc Ctx)andGetAtScope(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 deadScopeLocal6th tier was dropped entirely. No such symbols remain (grep in internal/modules/config + internal/core empty). Rewrite againstcore/scope.Scope {global,tenant,environment,stack,deployment}.
docs/architecture/core/daemon-manager.md
Section titled “docs/architecture/core/daemon-manager.md”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 dropswirePhase35Seams. 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” ->wirePhase35Seamsno longer exists; onlywireModuleServices(services.go:123) andwireExtraModuleServices(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-
anycontract -> Document the Resolver/DepsBuilder typing model: every shared field is concretely typed (Ctx/Pool/Config/Logger/Paths/OtelMeter/OtelMeterProvider) and ONLYServicesstaysanyto 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.Servicescast to a consumer-definedxServicesinterface (schedule/register.go:50-72), GetRuntimeService only as fallback. Describe*daemon.Servicesas the composition root and the singletons as the migration bridge.
docs/architecture/diagrams/README.md
Section titled “docs/architecture/diagrams/README.md”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.mmdsources forinternal/core/types|internal/core/errors|internal/modelsreturns zero matches (including the two named offenders).
- README.md:68 — Diagram 14 graphs “the package dependency graph from
internal/core/types/andinternal/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.mmdsources (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 atmodules/task/models.go(ce module). (DONE 2026-07-31, this sweep: the key-files table now citesmodules/task/models.go+ent/schema/task.go; the page’s Pattern 5 RPC section was also rewritten againstmodules/internal_rpc+ the peer client, and the dead progress/rpc/internal_api paths were replaced.)
docs/architecture/modules/schedule.md
Section titled “docs/architecture/modules/schedule.md”- schedule.md:127 — “ScheduleModel (
internal/models/schedule.go)” -> internal/models does not exist; ScheduleModel is defined atmodules/schedule/models.go(ce module). (DONE 2026-07-31, this sweep: repointed, and the page’sSyncSchedules-in-app claims were rewritten tomodules/schedule/service.go::SyncApp.)
README.md
Section titled “README.md”- README.md:172 — Backend standards links to
docs/contributing/standards/backend/models.mdand.../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.)
docs/architecture/core/tech-stack.md
Section titled “docs/architecture/core/tech-stack.md”- 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:58still says “wave 1 — leaves … More land here”, and the docker accessor comment (now ~:559) still names adocker.resolveServicethat does not exist (noresolveServiceinmodules/dockerormodules/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.resolveServicereads instead ofdocker.GetRuntimeService().” -> docker has noresolveServiceand no injected read path;docker.NewRouterignores Deps (router.go:27). Reword: DockerService() is published for a future consumer but ALL current docker.Service reads go throughdocker.GetRuntimeService()(the bridge) — accessor is currently write-only. - composition.go:936-945 — CloneService() doc: “The injection point
clone.currentResolverreads instead ofclone.GetRuntimeService()per-request. The clone module’s HTTP router … resolves through the package serviceResolver.” -> Self-contradictory and false:clone.currentResolver()reads the package-globalresolveSv, neverd.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 readsd.Services/CloneService()(router.go:72 ignores Deps; resolves via packagecurrentResolver()); the onlyCloneService()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 21xServicesmodules.
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.LoggerintoBootstrapDeps. Replace the example with a subsystem that still registers a nil DepsBuilder, e.g. operation_registry or schedule_dispatcher.
modules/config/models.go (ce module)
Section titled “modules/config/models.go (ce module)”- (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
Environmenttoo, andScope()classifies into FIVE tiers (Global/Tenant/Environment/Stack/Deployment) viascope.Classify(models.go:36-52). Enumerate all five tiers or defer to theScope()godoc.
modules/vault/service.go (ce module)
Section titled “modules/vault/service.go (ce module)”- (re-verified 2026-07-31 — still live, now at :774) service.go:505 — “matching the legacy
scopeToSecretScopecase \"\":fallthrough.” — names a function that no longer exists ->scopeToSecretScopewas 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.”
modules/vault/paths.go (ce module)
Section titled “modules/vault/paths.go (ce module)”- (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_pathsin Python.” (paired with scope.go:16-18 claiming the canonical cascade “mirrors the historical vault path cascade (internal/modules/vault/paths.go)”) ->core/scope.Cascadeis now the single authority; vaultCascadePathsduplicates its ordering by hand rather than calling it. Update both comments to nameinternal/core/scope.Cascadeas the source instead of mutually referencing each other / Python.
core/coded/coded.go (ce module)
Section titled “core/coded/coded.go (ce module)”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 likeX200049LHRFRSH. 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.gofiles 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.
modules/stack/errors.go (ce module)
Section titled “modules/stack/errors.go (ce module)”- (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
stackErrorSpecshas 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
Section titled “.golangci.yml”- .golangci.yml:114-120 (forbidigo header comment) — “every error returned across a function boundary must be a
*coded.CodedError… Plainerrors.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 (nofmt.Errorfleft in cedaemon/job_executor_adapter.goormodules/app/commands/system_install.go); the hud validation-closure site survives asshc/modules/hud/install_flow.go:118— verify whether it still lacks its siblings’//nolintbefore 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” prescribedslog.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.Errorare forbidden in production; canonical form iscorelog.Warn(ctx, X200003OPFAIL())/corelog.Error(ctx, X500907STKFAL(), cause)passing the*coded.CodedErroritself (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
slogwith attributes” -> Production paths must use corelog’s typed API (corelog.Warn/Errorwith a*coded.CodedError), not rawslog. 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”.)
docs/guide/operations/observability.md
Section titled “docs/guide/operations/observability.md”- (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 thenos.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 toshclog.Debug/Info/Warnmessage arguments -> Uses the holdovershclogalias; the mandated alias iscorelog. corelog.Warn takes a*coded.CodedError, not a free-text “message argument” — the human string lives incoded.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.gono 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), andstartAPIServeris 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 issubsystems/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 (
appServiceFromDepsonregistry.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.gonow resolves off the composition root —resolveService(d coreregistry.Deps)readsregistry.Deps.Services. Verified 2026-07-31.) - registry.go:130 / daemon.go:697-700 comment — the dead-startAPIServer narration -> (RESOLVED:
startAPIServerand its flatten copy are deleted from the tree; the sole flatten narration lives insubsystems/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(thestack.Service.recordStackRowshape). - app/state_mirror.go:27-34 —
ReconcileDiskStacks(perpetual ticker + tombstone) replaced by the one-shotImportOrphanManifestsboot import; the out-of-band clone/move/recover writers callRegisterRecoveredStack(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 viaRegisterRecoveredStack. - stack/router.go (handleStatus) + app/router.go (stackStatusHandler) — single-owner confirmed:
stack/router.gocedesshc.stack.statusandapp/router.goowns it (unchanged by B1).
docs/contributing/standards/testing.md
Section titled “docs/contributing/standards/testing.md”- 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/integrationand the enumerated coverage paths.) testing.md:83 —go test -race ./...given as a how-to-run command ->./...walks the dh-golangobj-*/GOPATH tree (the Makefile enumerates paths explicitly at :306/:315/:320 to dodge it), and-racehere is also not how the suite enables it (integration lane probes and applies$(RACE), Makefile:165-173/:344). Replace with the enumeratedmake test/integrationform.
Makefile
Section titled “Makefile”- 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 listguard neutrally.)
modules/network/state.go (ce module)
Section titled “modules/network/state.go (ce module)”- 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.md — MOOT (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() callingdatabase.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 theimpl.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” -> Noregistry.SubsystemSETTER exists in internal/core/registry/registry.go (only an unusedsubsystemsmap + getter). Subsystems are registered via internal/subsystems/register.goRegisterAll()listingimpl.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:150EntClient() *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/jobs…jobsRegistry.Add(...)against the central registry” -> There is no internal/core/registry/jobs package. Crons are registered viaregistry.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
*routesstruct + newRoutes; tenant/register.go puts it in register.go with a freeresolveService+newRouter. The 20 files implement at least 4 distinct shapes (interface-in-register vs interface-in-router;*Servicevsfunc() *Serviceresolve;newRouterfree-func vs*routesstruct). The cross-reference is misleading.
modules/operation/router.go (ce module)
Section titled “modules/operation/router.go (ce module)”- (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:46type 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/scopeas 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
runningslice) 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 therunningslice, 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
runningslice 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 = 33insubsystems/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-ctxenforces 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 bysubsystem-loop-start-ctx. Verified via the fitness gate 2026-07-31.)
docs/architecture/feature-graph.yaml
Section titled “docs/architecture/feature-graph.yaml”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>.
Lens 1 — repo-docs
Section titled “Lens 1 — repo-docs”(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.DepsBuildergodoc cites bootstrap as the “Start with a nil deps argument” exemplar, now FALSE (bootstrap.go:65-71 has a non-nil DepsBuilder buildingBootstrapDeps{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
CascadePathsnow re-implementsscope.Cascade’s tier ordering by hand instead of calling it. The canonical authority is core/scope; neither comment names it.
Lens 3 — di-composition
Section titled “Lens 3 — di-composition”RESOLVED (2026-07-04) — DI sweep COMPLETE.
di-singletonsis a hard 0 and the dual-write bridge is deleted (ADR-0006 COMPLETE);composition.gois constructor-injected. The write-only accessors, un-rootedconnections/jobsingletons, the “29 bridge calls”, and theforwardwrite-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.resolveServicereads the injected root; no such function exists and docker NEVER readsd.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.currentResolverreads the injected root per-request; it reads the package-globalserviceResolver(resolveSv), notd.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.
Lens 4 — error-handling (coded errors)
Section titled “Lens 4 — error-handling (coded errors)”- 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 wiredSpec incore/coded/errors_core.go:414— the never-registered half is fixed; whether a divergent bare-coded.Newcopy 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.goslog.Warnitems (202/209/224) are MOOT —state_mirror.gowas deleted by ADR-0002 / B1 (reconciling with the “RESOLVED (ADR-0002 / B1)” banner in section (A)). Thecore/api/server.gostderr writes (264/349) are closed by fitness Check 11daemon-stderr-ban(hard 0). The remaining plain-string.Warn(/.Error(items are now capped by fitness Check 13untyped-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.Warnfor “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.Warnfor “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 becorelog.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 codedcorelog.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 mandatescorelog; 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.Errorcalls in CLI surfaces using the plain.ErrorMETHOD form that escapes forbidigo’s.ErrorContextban. -> RESOLVED (2026-07-06) by the CLI double-emit fix (e22cda06c): all four.Errorcall 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.
Lens 6 — http-routing
Section titled “Lens 6 — http-routing”Duplicate-route items RESOLVED (single-owner now).
shc.stack.statusis owned by the app module (stack cedes it —stack/router.go),shc.stack.moveis owned by the clone module (stack cedes it), and theshc.backup.retention.*routes moved to the retention module (backup no longer registers them). The deadstartAPIServerthird 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.statusregistered by TWO modules (app svc-bound stackStatusHandler; stack resolver-bound handleStatus). Both blank-imported; the flatten loop iterates a Go MAP (non-deterministic) andPublicRouter().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.moveregistered 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
*ServiceEAGERLY at build time (newRouter(d, GetRuntimeService())); handlers close over the captured svc. Captures nil if wiring order changes; ignoresd.Servicesfor 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 thed.Servicesconsumer interface; the builder takes_ coreregistry.Depsand 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.Servicesto 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
DialContextunix closures re-implement whatcore/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.gois deleted,app.Stateis demoted to the offline/test store (not a co-equal writer), and the appServiceroutes every stack read/write through one activedeploymentStore(the DBdeploymenttable). The mirror funcs, the perpetual disk-reconcile ticker, and the tombstone machinery are gone;ReconcileDiskStacksis a one-shotImportOrphanManifestsboot 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”.
Lens 8 — testing conventions
Section titled “Lens 8 — testing conventions”RESOLVED (2026-07-04) — the global-swap sweep landed.
di-test-fixturesis a hard 0 (.SetRuntimeService(in*_test.go= 0; theSetRuntimeServicesetters no longer exist), so the 327-site SWEEP BLOCKER and the parallel-plus-global-swap data-race items below are closed. (Thedbtest_setup_test.gocopy-paste and low-t.Parallelnotes 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.gofiles inject fakes by swapping a package globalX.SetRuntimeService(fake)instead of constructing the service and passing throughregistry.Deps{Services:...}. This exercises only the LEGACY bridge; the canonicald.Services.(iface)path (23 production routers prefer it) is NEVER hit (0 tests set Deps.Services). Blocks deleting the 30 productionSetRuntimeServicesetters (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 ofruntimeSvraces the parallel test’s read of GetRuntimeService. Latent only because-raceis 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 sharedtestsupport.LockRuntimehelper 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.goboilerplate copy-pasted into 16 module test packages — identical 8-line blank-import side-effect file differing only in thepackage X_testline. 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_mountbug, 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.shsubsystem-loop-start-ctxis a hard 0 (non-detached-ctxcontext.WithCancel(ctx)insubsystems/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 asrs.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.
Lens 10 — module-structure
Section titled “Lens 10 — module-structure”- 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
resolveServicehelper has TWO incompatible signatures: returns*Servicein 7 modules (audit/logs/backup/network/schedule/tenant/config) vsfunc() *Servicethunk 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 freenewRouter(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
StorenotService, 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 unexportedmoduleName, 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
*apisubmodule 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 throughregistry.Deps.Services, but 6-7 modules declare NO consumer interface and never readd.Services— they resolve only through a legacy package-levelSetServiceResolver(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 packagecurrentResolver()singleton, neverCloneService(). 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 takefunc() *vault.Servicereturning the concrete type. The DI plan promised consumer interfacesconnections.VaultServiceandjob.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 consumerxServicesinterfaces are bound to*daemon.ServicesONLY by runtimed.Services.(xServices)assertions. There is NOT ONEvar _ scheduleServices = (*Services)(nil). If a*Servicesaccessor signature drifts/renames, the assertion silently fails at runtime and falls back to the legacy singleton — the opposite of forward’s gold-standardvar _ 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
xSourceinterface 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 anxServicesinterface or a single Source interface — an outlier shape no other module uses, bundling three unrelated capabilities into one positional-tuple resolver.
(C) ENFORCED vs PENDING fitness rules
Section titled “(C) ENFORCED vs PENDING fitness rules”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 fordi-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
*.SetRuntimeServicepackage globals are fully deleted; a new one failsmake 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 thestack.RuntimeStatesweep). 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 oftypes.ScopeTenant/ScopeLocal/the dead 6th tier). - no-cgo-sqlite (hard) — no
mattn/go-sqlite3import ininternal//cmd/; only pure-Gomodernc.org/sqlite(driver “sqlite”), keeping the hermeticCGO_ENABLED=0build buildable (Lens 8 rule #3). - layer-boundary (hard) — no provider imports a consumer (impl never imports daemon; no
internal/modules/*non-test file importsinternal/daemon) (Lens 12 rule A). - core-imports-modules (hard) —
internal/coremust not importinternal/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 outsideinternal/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/implloop 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 (outsidecommands/and theerrors*.gofactory 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, …)ininternal/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=valuelines 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 typedcorelog. 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
→ WIREDinline): Lens 9 NO-START-CTX-CAPTURE (assubsystem-loop-start-ctx), Lens 5 ESCAPE-HATCH-GREP (split intodaemon-stderr-ban+untyped-logger), and the Lens 4 coded-error hygiene proposals (partly, asinline-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.mdmust 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
.mdlink 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.gomust 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 guardedif 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 newanyResolver 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 runto 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 -dMUST 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.LookupAND 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; theslog.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 barelogandshclog).
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 -dMUST 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 _testMUST 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/..."thensort | uniq -dmust 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 -lmust be monotonically NON-INCREASING; end-stategit grep -n 'mirrorStackRow\|state_mirror.go' internal/modules/app/state.gomust 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 onlybuildRouter(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 -uMUST yield exactly ONE signature line (today:*Serviceandfunc() *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.goandgrep -rn 'registry.Router("' internal/modules/*/register.goMUST 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 ENFORCEDuds-dialer-copiesratchet 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.Parseexists,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 SetServiceResolverAND lacks atype [a-z]*Services interfacein 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 _testpinned 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 everytype [a-z]*Services interfacein 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 (
launcherkey, deployment > stack > environment > tenant > global, then the app.yamllauncher:default) — thelauncher *boolstate hybrid (models/types/repository/CLI--launcher/--no-launcher) was deleted and thedeployment.launchercolumn retired in place (migrations frozen). Injection rides the shc-oidc gating seam:writeIngressRouteFilechains + defines a per-deploymentshc-launcher-<project>rewritebody middleware in the file-provider route file (NOT docker labels — cross-node safe underexposedByDefault: false);MaterializeIngressre-resolves per sweep vialauncherForDeployment. Deployment-scopedlauncherconfig rows already rideBackupManifest.DeploymentConfiggenerically. -
shc cron lsoperator surface. RESOLVED:internal/modules/cronis the read-only view overregistry.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 samecrons.<name>override resolution the cron_runner applies (the registry records no fire history, so there is no last-run column). - Dead
events.OnCron/PendingCrons/CronRegistrationseam. -> (RESOLVED: the seam is gone — no OnCron/PendingCrons symbols remain inmodules/events/handlers.goat HEAD (deleted with the cron-system rework that made CronRunner the sole CronsAll consumer). Verified 2026-07-31.) -
docs/proposals/config-v2.yamlis stale. Itstimeouts/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 (likedi-migration-plan.md). -
config.go:299 SHC_KEYCLOAK_URL → keycloak.urldead env alias. -> (RESOLVED: noSHC_KEYCLOAK_URLreference remains incore/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 scopedService.Get(ctx,key,Ctx{…})walk (proxy.connectionwas retired by proxy-plane v2 — LB attachment lives in--port <conn>@…mappings now); every othercfg.String/Intreader 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.targetcomment drift (low). -> (RESOLVED:modules/app/dns_target.gonow opens with “there is NO scopeddns.targetconfig 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.shdefaultsSWAP_GB=4; bump to 8 and/or cap ceph mon/osd memory before chasing a fully-green proxmox run. Runensure-swap.sh --allafter 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
mainHEAD (an old run forked from a stale base predating the DI spine → empty no-op diffs); the judge shouldgit apply --checkagainst 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 isentNodeToTarget(13 columns). Two more (role,resources) are never written but ARE selected:scheduling_source.go:161-164SELECTs them into plain strings, so the NULL columns fail the Scan and thecontinueskips EVERY row — the metrics node-schedule source is structurally EMPTY in production. Fix the scheduler to stop selecting never-written columns (or scan throughsql.Null*) regardless of the wire-or-drop outcome. Related half-wire:availabilityIS durably written (drain’s SetAvailability → UpdateAvailability) but never read back —entNodeToTargetdoesn’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 hardcodesAvailability: "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_atlook 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 inent/schema/node.go:55-70with its own OWED comment, butgo generate ./ent+ the migration never ran: the generatedNodeTable.Annotationcarries NO Check (ent/migrate/schema.go — sibling tables’ checks ARE generated: Deployment/JobRun/Task), and no migrations/*.sql carriescluster_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/IsValidClusterStategate 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 unrelatedmake 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: rungo 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.