背景
接口声明已经放在调用方(agent/deps.ts 自己声明 14 个 Port),但 Port 的签名里直接引用了叶子模块的类型 ,于是"契约模块"反过来 import 了"实现模块"。
端口签名引用的类型就是抽象的一部分;它定义在实现包里,抽象就依赖了细节。后果是具体的:叶子改一个字段,agent/deps.ts 必须跟着改;任何替代实现(mock / 远程 executor)都被迫先 import tools/ 才能满足端口。倒置只剩名义。
同一时间,core/ 作为词汇层混装了三类东西:
通用词汇 :error.ts(AgentError / ErrorCode)、result.ts、path.ts 的路径运算 —— 不指向任何功能,任何模块都可依赖;
领域词汇 :types.ts 的 ProfileName / TodoItem / Message / ToolCall,全部指向具体功能;
不是词汇 :workspace.ts 是 export class WorkspaceService extends Effect.Service,带 init / loadConfig 实现,还 import { loadConfig } from '@codingcode/infra/config'(跨包)。
具体问题
1. 契约引用叶子类型
agent/deps.ts 的 14 个 Port 中 10 个引用了叶子模块的类型:
Port
泄漏类型
来源
ToolExecutorPort
ToolResultUnion、ToolLookup
tools/port.ts:6、:11
ToolCatalogPort、McpPort
ToolDefinition
tools/types.ts
SessionPort
SessionStoreState、UserEvent、AssistantEvent、ToolResultEvent
session/types.ts
ApprovalPort
ApprovalDecision、PermissionMode
approval/types.ts
HookPort
HookDecision;另 emit(point: string) 把 HookPoint 放宽成了 string
hooks/types.ts
SkillPort
Skill
skills/types.ts
ContextPort、MemoryPort、LlmPort
LLMClient
llm/client.ts
2. 一个概念多份定义
ToolResultUnion 在 tools/port.ts:6 与 tools/tools.ts:8 各一份;ToolLookup 两份且不一致(tools/port.ts:11 的 ToolDefinition<any> vs tools/tools.ts:13 的 ToolDefinition)。
ToolOutcome(core/frame.ts:30)与 ToolResultUnion 同构、判别名不同(status vs type),迫使 agent/agent.ts:334-338 写手工映射。
ToolCallRequest(approval/types.ts:23)已存在,但 approval/port.ts:7 与 agent/deps.ts 的 ApprovalPort 仍把同一对象各内联一遍。
3. 端口窄化:一个被误判的机制
早期版本判断"要把 SessionService(18 方法)收窄到 SessionPort(8 方法),适配器是唯一手段,所以删不得"。这不成立 —— 本仓自己就给出了反例:
session/port.ts:79-92 的两个消费视图用的是零成本手段:
// direct/sessions.ts 实际使用的消费视图,编译期锁定真实耦合面
export type SessionStorePort = Pick < SessionShape , 'create' | 'load' | ... > ;
// direct/agent-runtime.ts 与 direct/settings.ts 只读/只写权限模式
export type SessionStatePort = Pick < SessionShape , 'load' | 'setPermissionMode' > ;
零 Tag、零适配器、零运行时开销,编译后完全擦除。
准确的区分是:
想窄化的对象
手段
成本
消费者可见的方法面
Pick<Shape, …> 类型别名
0 (纯类型)
Effect 的 R 通道
必须新建 Tag —— R 通道由 Tag 决定
每对 1 个 Tag + 1 个适配器
而 R 通道窄化在本项目中没有消费者 :agent/port.ts 对外的 runTurn,其 R 已是 never(由 AgentWithDeps provide 掉)。于是 agent/deps.ts 这 14 个 Tag 的净作用只剩"agent.ts 内部的名义 R 标注",换来的却是 layer.ts:43-110 的 12 个只做 .bind 的适配器(AgentDepsAdapter 合并 12 个)、12 份与宽契约手工同步的类型定义,以及第 4、5 节两笔真实损失。
补充一条边界:ToolExecutorPort(1 方法)对 ToolExecutorShape(1 方法)只差 opts 的必填性,是零收益裁剪的极端例;同时它也说明签名收紧 (可选参数改必填)无法用 Pick 表达,需要写一个手写窄类型别名 —— 仍然是纯类型、零 Tag。
4. 窄化切掉了模块的自我管理面
HookShape(hooks/port.ts:6-13)的 6 个方法混装两种性质的成员:
面
方法
消费者
服务面
emit / emitDecision / disposeSession
agent、approval、tools、subagent
自我管理面
register / registerDecision / reloadUserHooks
配置变更事件、模块初始化
HookPort 按"agent 需要哪几个"切出来,只留服务面。代价是实打实的:
reloadUserHooks(projectPath) 是用户 YAML hook 的唯一装载入口 (hooks/hooks.ts:182,内部经 resolveHookConfigs 读 ~/.codingcode/hooks.yaml 与 <cwd>/.codingcode/hooks.yaml,写入 hooksByProject)。而 agent/agent.ts:52-54 的项目准备只剩两件:
52: rules.evictProjectRules(normalizedCwd);
54: yield* mcp.syncConnections(normalizedCwd)...
// reloadUserHooks 无对应调用
搬迁前的对照物还在旧产物里:dist/runtime/project-runtime.js 的 ProjectRuntimeService.prepareProject 是完整三件套 evictProjectRules / reloadUserHooks / syncConnections。
后果:reloadUserHooks 在整个 src/ 里没有调用路径;hooksByProject 零写入者;emit / emitDecision 遍历的 handler 集合恒为空 —— 用户 YAML hook 当前不生效 。
结论(窄化的边界条件) :按"消费者视角"切接口,会把被切模块的自我管理职责一起切掉。窄化之前必须先按"服务面 / 自我管理面"分栏,自我管理面不参与窄化。
5. 宽契约把测试逼向 as any
test/approval/pipeline.test.ts:9-20 的 mockHookService 声明了 10 个方法,其中 attachSessionHooks / disableHook / enableHook / disposeProject 在 HookShape(6 方法)里根本不存在 (旧版残留)。结果 :32、:33、:37 各一处 as any。
宽接口不给替身一个"必须满足的最小面",类型检查就只能关掉 —— 而 as any 同时把"漏写方法"从编译错降级为运行时 TypeError。
6. 其他越界项
McpPort.listProjectMcpTools(mcp/port.ts:11)返回裸值 ToolDefinition[],同 tag 其余方法都返回 Effect → 不可测试。
subagent/port.ts:11-12 内联 import('../core/types.js').ProfileName 与 import('../approval/types.js').PermissionMode。
client/contracts.ts:18 用 typeof AVAILABLE_PROFILES 从 agent/profile.js 引运行时值派生类型。
session/port.ts:79-92 的 SessionStorePort/SessionStatePort 机制正确(Pick),但落点方向反了 —— 消费视图应由消费者声明,不该由叶子导出。
解决思路
类型定义的落点由两层判据 决定,而不是"被谁调用":
判据一 —— 这个概念是否指向某个功能模块?
不指向(错误、结果容器、路径运算)→ 通用件 → core/
指向(MCP、权限、会话、工具、LLM…)→ 领域件 → 判据二
判据二 —— 引用它的模块数 / 包数
仅 1 个 src 领域引用 → 回该领域叶子
只出现在某调用方接口签名里 → 内联进调用方
≥2 个 src 领域,或 ≥1 个跨包 → 共享包
判据二只作用于领域件:AgentError、Result 这类无领域归属的类型不参与引用面计算,天然留在 core/,被几个模块引用都不改变这一点。
目标分层与四条硬规则:
层
目录
允许依赖
L0 通用
core/
无 import/import type/node 内置
L1 共享包
contracts/(新增)
仅 L0
L2 实现
tools/、hooks/、session/、approval/、llm/、mcp/ …
L0 + L1
L3 组合根
layer.ts + agent/tool-catalog.ts、agent/tool-env.ts
全部
R1 契约不得 import 实现 | R2 实现必须依赖契约模块,但不得依赖消费者模块 | R3 core/ 零内部依赖 | R4 一个概念只允许一处类型定义。
R2 不能写成"实现不得 import 契约"——VS Code(workbench/services/storage/browser/storageService.ts:17 → platform/storage/common/storage.js)与 NestJS(packages/core/nest-application.ts:30 → @nestjs/common)都是实现 import 契约 ,方向正是"细节依赖抽象"。(Effect 属另一形态:接口与运行时 token 同处顶层 Clock.ts,实现下沉 internal/,见参考来源。)会出现"实现不得 import 契约"这个反表述,主因是契约被放进了消费者模块内部 —— 本项目现在就是这样(L1 在 agent/deps.ts),代价已在第 3、4、5 节显形。
组合根落点说明:装配 Layer(agent/tool-catalog.ts、agent/tool-env.ts)就地保留在 agent/ ,不新建 src/composition/ 目录。它们的职责是"把若干服务折叠成一个 provide",属于组合根性质,但不值得为它单开一个顶层目录 —— 目录数量本身不是收益。
边界 :承载实现机制形状的类型(z.ZodTypeAny、SDK client、Effect 的 R 通道)必须留在叶子 —— 搬进共享层会让抽象反过来依赖机制库,那是另一种倒置。契约只暴露窄的纯数据描述。
具体方案
落点分配
落点
准入
接收的类型
core/(保留)
不指向任何功能模块
AgentError/ErrorCode、Result、normalizePath/encodeProjectPath/getProjectBaseDir/computePaths
src/contracts/(新增)
领域件,≥2 个 src 领域或跨包
PermissionMode/ApprovalDecision/PLAN_ALLOWED_TOOLS、ProfileName/TokenUsage/TodoItem/Message/ToolCall/ToolDescription、McpServerConfig/McpStatus/McpToolSpec、UserHookConfig/HookPoint/HookDecision、SessionEvent 家族/SessionStoreState/SessionIndex/UITurn、FrameBody/Frame/ToolOutcome、LLMClient/SelectableModel、ToolLookup/ToolResult/ToolRunner、Skill、ApiError/AlreadyExistsError/NotFoundError、CheckpointDiff 组
各领域叶子
领域件,仅本领域引用
ToolCallRequest/isPermissionMode→approval/;LLMRequest/LLMResponse/LLMStreamPart→llm/;FrameError/ResponseMeta/isTurnEnd/toolOutcomeOf→agent/;RollbackEvent/SessionMetaEvent/UserEvent/sessionJsonlPathFromCwd→session/;estimateTokens 组→context/;ToolExecCtx→tools/;encodeFrame→server/;decodeFrame→client/;parseWorkspaceArgs/isGlobalCwd→workspace/
调用方
仅出现在调用方签名
ApprovalRequest → 内联 agent/deps.ts
执行步骤
步骤
内容
S1 类型落点重划
core/ 只留通用件;workspace.ts(实现 + 跨包引 infra/config)与 util.ts(token 估算指向 LLM)移出;error.ts 的 HTTP 类移入共享包;删 path.ts 的 setProjectBaseDir/setProjectPlansBaseDir(测试钩子式可变全局)与零引用的 ModelInfo;新增 src/contracts/ 接收上表领域共享件,单引用领域件退回各自领域,ApprovalRequest 内联 agent/deps.ts。同步改完所有引用点,不留 re-export 垫片
S2 端口简化
14 个 Port 的类型来源 改用上表落点(删掉全部叶子 import);并删掉这 14 个 Context.Tag 与 layer.ts:43-110 的 12 个适配器 —— 可见面改用类型别名表达(agent/deps.ts 保留为纯类型文件),取服务时写 const hooks: AgentHooks = yield* HookService,R 通道留给宽 Tag,由 Layer.provide(HookLayer) 直接消掉,不需要 wiring。同时:HookPort.emit(point: string) 收回 HookPoint;McpPort.listProjectMcpTools 改为返回 Effect。唯一例外 :若某方法的返回值需真实转换(如 recordUser 从 UserEvent 收窄为 turnId: number),该处必须保留 Tag + 适配器 —— 值映射是 Pick 表达不了的能力
S3 去重
删 tools/tools.ts 的重复 ToolResultUnion/ToolLookup;共享层定义 ToolOutcome + ToolResult = { id, name } & ToolOutcome;判别名统一为 status,删掉 agent.ts:334-338 的手工映射(RuntimeEvent.tool_result 字段与帧格式零变化)
S4 装配归位
agent/tool-catalog.ts、agent/tool-env.ts 就地留在 agent/;McpPort 的真实映射(把 MCP 工具喂给 catalog)保留在装配层,不再是 .bind 直通
S5 引用收口与断言
更新 package.json exports 与 desktop/tui 的跨包引用(唯一值导入 ApiError 一并处理);session/port.ts:79-92 的消费视图移到消费者内声明;新增架构断言:core/ 不得出现跨领域 import、contracts/ 不得 import 领域实现、每条相对 import 必须能在仓库内找到落点
S6 接回断链
在 S2 之后,reloadUserHooks 已有调用位置(HookPort 不再切掉自我管理面),在 agent.ts 的项目准备处按 evictProjectRules → reloadUserHooks → syncConnections 顺序接回,并确认 emit 用的是本轮新注册表
参考来源
社区与官方
Go Wiki, Code Review Comments#interfaces — "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values." https://go.dev/wiki/CodeReviewComments#interfaces
Google Go Style Guide, Decisions#interfaces — 消费者定义接口;生产者仅在"接口即产品"时导出 https://google.github.io/styleguide/go/decisions#interfaces
Teiva Harsanyi《100 Go Mistakes》可扩展架构方案:为 Subagent、Hook、多端预留结构 #5 / 架构重设计 #6 — "Abstractions should be discovered, not created." / "Keeping interfaces on the client side avoids unnecessary abstractions." https://github.com/teivah/100-go-mistakes
Robert C. Martin, The Dependency Inversion Principle , C++ Report, May 1996 —— "Abstractions should not depend on details."
Derek Greer, Examining the Dependency Inversion Principle —— "the interfaces are packaged together with the higher-level components" http://www.lostechies.com/derekgreer/2008/12/28/examining-dependency-inversion/
Alistair Cockburn, Hexagonal Architecture (2005) https://alistair.cockburn.us/hexagonal-architecture/
Ben Johnson, Standard Package Layout —— "The root package should not depend on any other package in your application!" https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1
Grafana, contribute/backend/package-hierarchy.md —— "Keep interfaces and domain types to the root package." https://github.com/grafana/grafana/blob/main/contribute/backend/package-hierarchy.md
Effect 官方 Managing Services https://effect.website/docs/requirements-management/services/ | Context (Context.Tag 是 R 通道的决定者,Layer.effect 是 Tag → Layer 的唯一构造方式) https://effect.website/docs/requirements-management/context/ | Dave Cheney《Practical Go》§6.9 "Let callers define the interface they require" | Rob Pike, Go Proverbs
背景
接口声明已经放在调用方(
agent/deps.ts自己声明 14 个 Port),但 Port 的签名里直接引用了叶子模块的类型,于是"契约模块"反过来 import 了"实现模块"。端口签名引用的类型就是抽象的一部分;它定义在实现包里,抽象就依赖了细节。后果是具体的:叶子改一个字段,
agent/deps.ts必须跟着改;任何替代实现(mock / 远程 executor)都被迫先 importtools/才能满足端口。倒置只剩名义。同一时间,
core/作为词汇层混装了三类东西:error.ts(AgentError/ErrorCode)、result.ts、path.ts的路径运算 —— 不指向任何功能,任何模块都可依赖;types.ts的ProfileName/TodoItem/Message/ToolCall,全部指向具体功能;workspace.ts是export class WorkspaceService extends Effect.Service,带init/loadConfig实现,还import { loadConfig } from '@codingcode/infra/config'(跨包)。具体问题
1. 契约引用叶子类型
agent/deps.ts的 14 个 Port 中 10 个引用了叶子模块的类型:ToolExecutorPortToolResultUnion、ToolLookuptools/port.ts:6、:11ToolCatalogPort、McpPortToolDefinitiontools/types.tsSessionPortSessionStoreState、UserEvent、AssistantEvent、ToolResultEventsession/types.tsApprovalPortApprovalDecision、PermissionModeapproval/types.tsHookPortHookDecision;另emit(point: string)把HookPoint放宽成了stringhooks/types.tsSkillPortSkillskills/types.tsContextPort、MemoryPort、LlmPortLLMClientllm/client.ts2. 一个概念多份定义
ToolResultUnion在tools/port.ts:6与tools/tools.ts:8各一份;ToolLookup两份且不一致(tools/port.ts:11的ToolDefinition<any>vstools/tools.ts:13的ToolDefinition)。ToolOutcome(core/frame.ts:30)与ToolResultUnion同构、判别名不同(statusvstype),迫使agent/agent.ts:334-338写手工映射。ToolCallRequest(approval/types.ts:23)已存在,但approval/port.ts:7与agent/deps.ts的ApprovalPort仍把同一对象各内联一遍。3. 端口窄化:一个被误判的机制
早期版本判断"要把
SessionService(18 方法)收窄到SessionPort(8 方法),适配器是唯一手段,所以删不得"。这不成立 —— 本仓自己就给出了反例:session/port.ts:79-92的两个消费视图用的是零成本手段:零 Tag、零适配器、零运行时开销,编译后完全擦除。
准确的区分是:
Pick<Shape, …>类型别名而 R 通道窄化在本项目中没有消费者:
agent/port.ts对外的runTurn,其 R 已是never(由AgentWithDepsprovide 掉)。于是agent/deps.ts这 14 个 Tag 的净作用只剩"agent.ts内部的名义 R 标注",换来的却是layer.ts:43-110的 12 个只做.bind的适配器(AgentDepsAdapter合并 12 个)、12 份与宽契约手工同步的类型定义,以及第 4、5 节两笔真实损失。补充一条边界:
ToolExecutorPort(1 方法)对ToolExecutorShape(1 方法)只差opts的必填性,是零收益裁剪的极端例;同时它也说明签名收紧(可选参数改必填)无法用Pick表达,需要写一个手写窄类型别名 —— 仍然是纯类型、零 Tag。4. 窄化切掉了模块的自我管理面
HookShape(hooks/port.ts:6-13)的 6 个方法混装两种性质的成员:emit/emitDecision/disposeSessionregister/registerDecision/reloadUserHooksHookPort按"agent 需要哪几个"切出来,只留服务面。代价是实打实的:reloadUserHooks(projectPath)是用户 YAML hook 的唯一装载入口(hooks/hooks.ts:182,内部经resolveHookConfigs读~/.codingcode/hooks.yaml与<cwd>/.codingcode/hooks.yaml,写入hooksByProject)。而agent/agent.ts:52-54的项目准备只剩两件:搬迁前的对照物还在旧产物里:
dist/runtime/project-runtime.js的ProjectRuntimeService.prepareProject是完整三件套evictProjectRules/reloadUserHooks/syncConnections。后果:
reloadUserHooks在整个src/里没有调用路径;hooksByProject零写入者;emit/emitDecision遍历的 handler 集合恒为空 —— 用户 YAML hook 当前不生效。5. 宽契约把测试逼向
as anytest/approval/pipeline.test.ts:9-20的mockHookService声明了 10 个方法,其中attachSessionHooks/disableHook/enableHook/disposeProject在HookShape(6 方法)里根本不存在(旧版残留)。结果:32、:33、:37各一处as any。宽接口不给替身一个"必须满足的最小面",类型检查就只能关掉 —— 而
as any同时把"漏写方法"从编译错降级为运行时 TypeError。6. 其他越界项
McpPort.listProjectMcpTools(mcp/port.ts:11)返回裸值ToolDefinition[],同 tag 其余方法都返回Effect→ 不可测试。subagent/port.ts:11-12内联import('../core/types.js').ProfileName与import('../approval/types.js').PermissionMode。client/contracts.ts:18用typeof AVAILABLE_PROFILES从agent/profile.js引运行时值派生类型。session/port.ts:79-92的SessionStorePort/SessionStatePort机制正确(Pick),但落点方向反了 —— 消费视图应由消费者声明,不该由叶子导出。解决思路
类型定义的落点由两层判据决定,而不是"被谁调用":
判据一 —— 这个概念是否指向某个功能模块?
core/判据二 —— 引用它的模块数 / 包数
判据二只作用于领域件:
AgentError、Result这类无领域归属的类型不参与引用面计算,天然留在core/,被几个模块引用都不改变这一点。目标分层与四条硬规则:
core/import type/node 内置contracts/(新增)tools/、hooks/、session/、approval/、llm/、mcp/…layer.ts+agent/tool-catalog.ts、agent/tool-env.tsR1 契约不得 import 实现 | R2 实现必须依赖契约模块,但不得依赖消费者模块 | R3
core/零内部依赖 | R4 一个概念只允许一处类型定义。R2 不能写成"实现不得 import 契约"——VS Code(
workbench/services/storage/browser/storageService.ts:17→platform/storage/common/storage.js)与 NestJS(packages/core/nest-application.ts:30→@nestjs/common)都是实现 import 契约,方向正是"细节依赖抽象"。(Effect 属另一形态:接口与运行时 token 同处顶层Clock.ts,实现下沉internal/,见参考来源。)会出现"实现不得 import 契约"这个反表述,主因是契约被放进了消费者模块内部 —— 本项目现在就是这样(L1 在agent/deps.ts),代价已在第 3、4、5 节显形。组合根落点说明:装配 Layer(
agent/tool-catalog.ts、agent/tool-env.ts)就地保留在agent/,不新建src/composition/目录。它们的职责是"把若干服务折叠成一个 provide",属于组合根性质,但不值得为它单开一个顶层目录 —— 目录数量本身不是收益。边界:承载实现机制形状的类型(
z.ZodTypeAny、SDK client、Effect 的 R 通道)必须留在叶子 —— 搬进共享层会让抽象反过来依赖机制库,那是另一种倒置。契约只暴露窄的纯数据描述。具体方案
落点分配
core/(保留)AgentError/ErrorCode、Result、normalizePath/encodeProjectPath/getProjectBaseDir/computePathssrc/contracts/(新增)PermissionMode/ApprovalDecision/PLAN_ALLOWED_TOOLS、ProfileName/TokenUsage/TodoItem/Message/ToolCall/ToolDescription、McpServerConfig/McpStatus/McpToolSpec、UserHookConfig/HookPoint/HookDecision、SessionEvent家族/SessionStoreState/SessionIndex/UITurn、FrameBody/Frame/ToolOutcome、LLMClient/SelectableModel、ToolLookup/ToolResult/ToolRunner、Skill、ApiError/AlreadyExistsError/NotFoundError、CheckpointDiff组ToolCallRequest/isPermissionMode→approval/;LLMRequest/LLMResponse/LLMStreamPart→llm/;FrameError/ResponseMeta/isTurnEnd/toolOutcomeOf→agent/;RollbackEvent/SessionMetaEvent/UserEvent/sessionJsonlPathFromCwd→session/;estimateTokens组→context/;ToolExecCtx→tools/;encodeFrame→server/;decodeFrame→client/;parseWorkspaceArgs/isGlobalCwd→workspace/ApprovalRequest→ 内联agent/deps.ts执行步骤
core/只留通用件;workspace.ts(实现 + 跨包引infra/config)与util.ts(token 估算指向 LLM)移出;error.ts的 HTTP 类移入共享包;删path.ts的setProjectBaseDir/setProjectPlansBaseDir(测试钩子式可变全局)与零引用的ModelInfo;新增src/contracts/接收上表领域共享件,单引用领域件退回各自领域,ApprovalRequest内联agent/deps.ts。同步改完所有引用点,不留 re-export 垫片Context.Tag与layer.ts:43-110的 12 个适配器 —— 可见面改用类型别名表达(agent/deps.ts保留为纯类型文件),取服务时写const hooks: AgentHooks = yield* HookService,R 通道留给宽 Tag,由Layer.provide(HookLayer)直接消掉,不需要 wiring。同时:HookPort.emit(point: string)收回HookPoint;McpPort.listProjectMcpTools改为返回Effect。唯一例外:若某方法的返回值需真实转换(如recordUser从UserEvent收窄为turnId: number),该处必须保留 Tag + 适配器 —— 值映射是Pick表达不了的能力tools/tools.ts的重复ToolResultUnion/ToolLookup;共享层定义ToolOutcome+ToolResult = { id, name } & ToolOutcome;判别名统一为status,删掉agent.ts:334-338的手工映射(RuntimeEvent.tool_result字段与帧格式零变化)agent/tool-catalog.ts、agent/tool-env.ts就地留在agent/;McpPort的真实映射(把 MCP 工具喂给 catalog)保留在装配层,不再是.bind直通package.jsonexports 与 desktop/tui 的跨包引用(唯一值导入ApiError一并处理);session/port.ts:79-92的消费视图移到消费者内声明;新增架构断言:core/不得出现跨领域 import、contracts/不得 import 领域实现、每条相对 import 必须能在仓库内找到落点reloadUserHooks已有调用位置(HookPort不再切掉自我管理面),在agent.ts的项目准备处按evictProjectRules→reloadUserHooks→syncConnections顺序接回,并确认emit用的是本轮新注册表参考来源
社区与官方
contribute/backend/package-hierarchy.md—— "Keep interfaces and domain types to the root package." https://github.com/grafana/grafana/blob/main/contribute/backend/package-hierarchy.mdContext.Tag是 R 通道的决定者,Layer.effect是Tag → Layer的唯一构造方式) https://effect.website/docs/requirements-management/context/ | Dave Cheney《Practical Go》§6.9 "Let callers define the interface they require" | Rob Pike, Go Proverbs