关于 effect-best-practices
effect-best-practices 是面向使用 Effect 构建 TypeScript 应用团队的编码技能,提供服务、错误、组合、Schema 和资源生命周期方面的约定与诊断流程。
主要功能
- 01Effect 服务与依赖的指导
- 02类型化错误定义和处理模式
- 03Schema 与品牌 ID 约定
- 04Effect.fn 与组合方式指导
- 05通过命令行运行语言服务器诊断
- 06资源作用域与清理实践
适用场景
- 01按项目约定审查 Effect 代码
- 02在 TypeScript 编辑后运行诊断
- 03选择类型化错误和服务模式
- 04组合 Layer 并管理资源清理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ forcedotcom/ salesforcedx-vscode --skill effect-best-practices安装后在 Claude Code 中输入/effect-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdcomposition-style.mddiagnostics-findings.mderror-patterns.mdlayer-patterns.mdobservability-patterns.mdschema-patterns.mdservice-patterns.md详细介绍
若要根据这些模式审查差异或计划,请调用 effect-advocate 子 Agent(.claude/agents/effect-advocate.md)。
Effect LS 诊断(Agent 用法)
Cursor 的 read_lints 不会显示 Effect Language Server 诊断。请使用 CLI:
npx effect-language-service diagnostics --file <path>
# or whole project:
npx effect-language-service diagnostics --project tsconfig.json
- PostToolUse
verify-on-edit.sh钩子会在每次.tsEdit/Write 后自动运行--file <edited>,并通过followup_message显示输出。请处理报告的问题。 - 警告和提示都要处理,而不仅是错误。
references/diagnostics-findings.md将常见问题映射到相应的修复方法;config/effect-diagnostics.json中的enforcedRules是构建门禁。 - 强制执行一条规则需要进行两处编辑,而不只是添加
enforcedRules条目——请参阅references/diagnostics-findings.md。 - 批量编辑后,请针对受影响的包运行
--project tsconfig.json,以发现跨文件问题。 effect-language-service quickfixes会显示建议的代码更改。
快速参考:关键规则
| 类别 | 应该 | 不应该 |
|---|---|---|
| 服务 | Effect.Service with accessors: true | 将 Context.Tag 用于业务逻辑 |
| 依赖项 | 在服务中使用 dependencies: [Dep.Default] | 在使用处手动调用 Layer.provide |
| 错误 | 使用带 message 字段的 Schema.TaggedError | 使用普通类或通用 Error |
| 错误具体性 | 当捕获分支或字段形状不同时拆分标签;遥测维度作为同一标签的字段 | 添加最终都输出 message / 触发相同 span 的额外标签 |
| 错误处理 | 使用 catchTag/catchTags;仅在需要时捕获 | 使用 catchAll;吞掉错误;“以防万一”而捕获 |
| ID | Salesforce 记录/org:SalesforceId/OrgId(core/schemas/salesforceId.ts)。DefaultOrgInfoSchema.orgId/devHubOrgId:使用 Schema.optional(OrgId),与 cliId 一样。其他情况使用 Schema.UUID.pipe(Schema.brand("@App/EntityId")) | 使用普通 string;临时使用 getAuthInfoFields().orgId;在 DefaultOrgInfo 上将 optionalWith 用作 Option |
| 函数 | 优先使用 Effect.fn 而非 Effect.gen;仅共享管道使用 .gen | 匿名生成器;使用嵌套 Effect.gen 添加恢复逻辑;用 .gen 编写业务逻辑 |
| 组合 | 使用 .pipe;对 yield 的值使用 local/inline-single-use-yield-binding;对 Effect/Stream/Schema 值使用 local/no-standalone-pipeable-value。详情:references/composition-style.md | — |
| 参数与依赖项 | 参数 = 运行时数据;依赖项 = 从上下文 yield 获取 | 将 Ref/PubSub/服务作为参数传递 |
| 命名 | 命令使用 FooCommand,辅助函数使用领域名称 | 使用 FooEffect 后缀(多余;TS/Effect.fn 已能表明类型) |
| 日志记录 | 使用带结构化数据的 Effect.log | 使用 console.log |
| 配置 | 使用带验证的 Config.* | 直接使用 process.env(构建时变量如 ESBUILD_* 除外) |
| 时间值 | 使用 Duration.seconds(30)、Duration.millis(5000);参数使用 Duration.DurationInput。local/no-raw-duration 仅针对 Duration / DurationInput 位置上的数字参数/属性强制执行。bigint、模板字符串、number 参数和 TIMEOUT_MS 常量不受此规则约束。 | 将毫秒数作为 number 参数,或使用 TIMEOUT_MS = 30_000 常量 |
| 选项 | 使用同时处理两种情况的 Option.match | 使用 Option.getOrThrow |
| 可空性 | 在领域类型中使用 Option<T> | 使用 null/undefined |
| Atoms | 在组件外使用 Atom.make | 在渲染过程中创建 atom |
| Atom 状态 | 对全局状态使用 Atom.keepAlive | 持久状态忘记使用 keepAlive |
| Atom 更新 | 在 React 组件中使用 useAtomSet | 在 React 中命令式调用 Atom.update |
| Atom 清理 | 对副作用使用 get.addFinalizer() | 未清理事件监听器 |
| 资源清理 | 使用作用域服务/层 + Effect.addFinalizer | 返回 dispose;将由 Effect 管理的资源委托给调用方 |
| Atom 结果 | 使用带 onErrorTag 的 Result.builder | 忽略加载/错误状态 |
| 分组 | 使用 Arr.groupBy(effect/Array) | 使用 Object.groupBy,其 Partial<Record> 会强制进行筛选 |
服务定义模式
业务逻辑服务始终使用 Effect.Service。这样可以自动生成访问器、内置 Default 层,并正确声明依赖。
import { Effect } from 'effect';
export class UserService extends Effect.Service<UserService>()('UserService', {
accessors: true,
dependencies: [UserRepo.Default, CacheService.Default],
effect: Effect.gen(function* () {
const repo = yield* UserRepo;
const cache = yield* CacheService;
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
const cached = yield* cache.get(id);
if (Option.isSome(cached)) return cached.value;
const user = yield* repo.findById(id);
yield* cache.set(id, user);
return user;
});
const create = Effect.fn('UserService.create')(function* (data: CreateUserInput) {
const user = yield* repo.create(data);
yield* Effect.log('User created', { userId: user.id });
return user;
});
return { findById, create };
})
}) {}
// Usage - dependencies are already wired
const program = UserService.findById(userId);
// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);
适合使用 Context.Tag 的情况:
- 运行时注入的基础设施(Cloudflare KV、worker 绑定)
- 资源由外部提供的工厂模式
- 由调用方提供实现的接口——没有唯一规范实现可打包为
.Default(例如,SoqlBuilderService分别由 VS Code 宿主和测试替身实现);请参阅references/service-patterns.md
参数与依赖
- 参数 = 每次调用时的运行时数据(ID、用户输入、单次调用配置)
- 依赖 = 共享基础设施(Ref、PubSub、SubscriptionRef、服务)——通过层提供,在 effect 内部 yield
- 在层中构建 Ref/PubSub 等(例如
buildAllServicesLayer);消费者应 yield 它们,而不是将它们作为参数接收
// WRONG - passing shared infra as params
const createStatusBar = (pubsub: PubSub.PubSub<void>, stateRef: SubscriptionRef.SubscriptionRef<State>) =>
Effect.gen(...)
// Caller must create and pass; wiring scattered at call sites
// CORRECT - yield inside, build in layer
const PubSubTag = Context.GenericTag<PubSub.PubSub<void>>("PubSub")
const createStatusBar = Effect.gen(function* () {
const pubsub = yield* PubSubTag
const stateRef = yield* StateRefTag
// ...
})
// Layer: Layer.effect(PubSubTag, PubSub.sliding<void>(1))
详细模式请参阅 references/service-patterns.md。
错误定义模式
错误始终使用 Schema.TaggedError。这样可以使错误可序列化(RPC 所必需),并提供一致的结构。
import { Schema } from 'effect';
import { HttpApiSchema } from '@effect/platform';
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
'UserNotFoundError',
{
userId: UserId,
message: Schema.String
},
HttpApiSchema.annotations({ status: 404 })
) {}
export class UserCreateError extends Schema.TaggedError<UserCreateError>()(
'UserCreateError',
{
message: Schema.String,
cause: Schema.optional(Schema.String)
},
HttpApiSchema.annotations({ status: 400 })
) {}
错误处理——使用 catchTag/catchTags:
// CORRECT - preserves type information
yield *
repo.findById(id).pipe(
Effect.catchTag('DatabaseError', err =>
Effect.fail(new UserNotFoundError({ userId: id, message: 'Lookup failed' }))
),
Effect.catchTag('ConnectionError', err =>
Effect.fail(new ServiceUnavailableError({ message: 'Database unreachable' }))
)
);
// CORRECT - multiple tags at once
yield *
effect.pipe(
Effect.catchTags({
DatabaseError: err => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),
ValidationError: err => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message }))
})
);
何时捕获(以及何时不捕获)
大多数错误都会呈现给用户(运行时通过消息/Toast)。仅在以下情况捕获:
- 确实可以忽略——接受失败并继续(例如,可选的预创建操作)
- 提供更好的消息——默认消息过于含糊;将其映射为更清晰的领域错误
对于预期跳过的情况(缺少可选插件、版本不兼容):在成功路径上写入 nls(guard / Match 分支)。fail+catchTag 用于意外情况的恢复——处理逻辑不应只是“打印这段字符串”。
谨慎捕获。不要使用 catchAll,也不要为了“安全起见”而吞掉错误。使用 catchTag/catchTags;记录日志,或以更明确的错误失败。
优先使用明确错误,而非通用错误
如果捕获分支或字段形状不同,就拆分标签——例如,message+cause 与 message+cause+setting。若捕获处理相同(打印 message,一个 span),就使用一个标签;遥测维度应是字段,而不是标签。nls 的差异放在 message 中。当前端/RPC 中的 UI 会分支时,仍应拆分(UserNotFoundError 与 ChannelNotFoundError)。请参阅 references/error-patterns.md。
在集合中累积错误
若要在失败后继续,而不是遇到第一个错误就短路,不要手动组合 Either + catchTag + 重新循环。请使用 Effect.partition(两个结果桶)、Effect.validateAll(要么全部成功,要么全部失败)或 Effect.validateFirst。这些方法会恢复每个项目的类型化错误通道,但不会捕获中断——因此 Cancel 仍会中止整个循环。
关于错误累积与中断的细节、错误映射和重试模式,请参阅 references/error-patterns.md。
Schema 与品牌类型模式
为所有实体 ID 添加品牌,确保跨服务边界的类型安全。
本仓库中,Salesforce 记录 ID 和组织 ID 不是 UUID。使用 SalesforceId/OrgId 和 orgIdFrom/orgIdFromConnection(getFields() / Connection;Option;references/schema-patterns.md)。其他 AuthFields:authFieldsFrom/authFieldsFromConnection。DefaultOrgInfoSchema.orgId/devHubOrgId:使用 Schema.optional(OrgId),与 cliId 一样——不要使用 Option。
import { Schema } from 'effect';
// Entity IDs - always branded
export const UserId = Schema.UUID.pipe(Schema.brand('@App/UserId'));
export type UserId = Schema.Schema.Type<typeof UserId>;
export const TenantId = Schema.UUID.pipe(Schema.brand('@App/TenantId'));
export type TenantId = Schema.Schema.Type<typeof TenantId>;
// Domain types - use Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
tenantId: TenantId,
createdAt: Schema.DateTimeUtc
});
export type User = Schema.Schema.Type<typeof User>;
// Input types for mutations
export const CreateUserInput = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
name: Schema.String.pipe(Schema.minLength(1)),
tenantId: TenantId
});
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>;
不应添加品牌的情况:
- 不跨越服务边界的简单字符串(URL、文件路径)
- 基本类型的配置值
关于转换和高级模式,请参阅 references/schema-patterns.md。
函数模式:优先使用 Effect.fn,而非 Effect.gen
对有副作用的代码优先使用 Effect.fn。它会自动进行追踪,并使用正确的 span 名称。必须提供 span 名称;由 local/require-effect-fn-span-name 强制执行。捕获中间件之前应放置 Effect.tap / *SuccessNotification 中间件:local/effect-fn-catch-middleware-last(之后的捕获和非成功守卫仍然有效)。详见 services-extension-consumption 中的 Success 处理。
仅在以下情况使用 Effect.gen: 需要共享一个附带通用 .pipe 的 Effect,以避免多个消费者重复对同一内容进行 pipe——例如,提供依赖、通用错误处理器、重试。(使用 Runtimes 时,这种情况较少见。)服务定义主体也是有效的用法(共享 wiring)。
// CORRECT - Effect.fn with descriptive name
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
yield* Effect.annotateCurrentSpan('userId', id);
return yield* repo.findById(id);
});
// CORRECT - Effect.fn with multiple parameters
const transfer = Effect.fn('AccountService.transfer')(function* (fromId: AccountId, toId: AccountId, amount: number) {
yield* Effect.annotateCurrentSpan('fromId', fromId);
yield* Effect.annotateCurrentSpan('toId', toId);
yield* Effect.annotateCurrentSpan('amount', amount);
// ...
});
// WRONG - params on wrapper arrow, generator has none (closure capture)
// Enforced by local/no-effect-fn-wrapper
const findByIdBad = (id: UserId) =>
Effect.fn('UserService.findById')(function* () {
yield* repo.findById(id); // id from closure
});
// WRONG - Effect.fn invoked immediately (config-enforced effectFnIife). Effect.fn builds a reusable
// function; for one-shot use write Effect.gen and keep the span with a piped withSpan.
const opened = Effect.fn('FsService.open')(function* () {
yield* fs.showTextDocument(uri);
})();
// CORRECT
const openedOk = Effect.gen(function* () {
yield* fs.showTextDocument(uri);
}).pipe(Effect.withSpan('FsService.open'));
// Naming: Don't append Effect. For commands use FooCommand; for helpers/lifecycle use domain names.
// WRONG: logGetEffect, executeAnonymousDocumentEffect, activateEffect
// CORRECT: logGetCommand, executeAnonymousCommand, executeAnonymous (helper), activation (lifecycle)
参见 references/composition-style.md:.pipe、单次使用 yield 值的 local/inline-single-use-yield-binding、终端 runner、point-free 安全性、Match 分派、守卫子句、对子序列进行恢复。
Layer 组合
在服务中声明依赖,而不是在使用位置声明:
// CORRECT - dependencies in service definition
export class OrderService extends Effect.Service<OrderService>()('OrderService', {
accessors: true,
dependencies: [UserService.Default, ProductService.Default, PaymentService.Default],
effect: Effect.gen(function* () {
const users = yield* UserService;
const products = yield* ProductService;
const payments = yield* PaymentService;
// ...
})
}) {}
// At app root - simple merge
const AppLive = Layer.mergeAll(
OrderService.Default,
// Infrastructure layers (intentionally not in dependencies)
DatabaseLive,
RedisLive
);
有关测试 layers 和依赖配置的 layers,请参见 references/layer-patterns.md。
由 Effect 管理的资源
在 Effect 服务/layer 内创建的资源属于其作用域。优先使用 scoped 搭配
Effect.addFinalizer 或 Effect.acquireRelease;不要暴露 dispose,也不要将清理工作交给宿主生命周期。
export class StatusService extends Effect.Service<StatusService>()('StatusService', {
accessors: true,
scoped: Effect.gen(function* () {
const item = vscode.window.createStatusBarItem();
yield* Effect.addFinalizer(() => Effect.sync(() => item.dispose()));
return { show: Effect.sync(() => item.show()) };
})
}) {}
Layer 的所有者必须关闭其作用域。ManagedRuntime 拥有其 layers 的作用域;请在扩展停用时对其进行释放。仅对在 Effect 所有权范围之外创建的资源使用 context.subscriptions。
Option 处理
绝不要使用 Option.getOrThrow。始终显式处理两种情况:
// CORRECT - explicit handling
yield *
Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: 'Not found' })),
onSome: user => Effect.succeed(user)
});
// CORRECT - with getOrElse for defaults
const name = Option.getOrElse(maybeName, () => 'Anonymous');
// CORRECT - Option.map for transformations
const upperName = Option.map(maybeName, n => n.toUpperCase());
Effect Atom(前端状态)
通过 @effect-atom/atom-react 管理响应式 React 状态。在组件外定义 atoms;需要持久化的状态使用 keepAlive;使用 useAtomSet 写入;使用 Result.builder 渲染有副作用的结果;使用 get.addFinalizer 清理监听器。
import { Atom, Result, useAtomValue, useAtomSet } from '@effect-atom/atom-react';
const countAtom = Atom.make(0); // outside the component
const prefsAtom = Atom.make({ theme: 'dark' }).pipe(Atom.keepAlive); // persistent
function Counter() {
const count = useAtomValue(countAtom);
const setCount = useAtomSet(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Effectful atom → Result; handle loading/error/success
function UserProfile() {
return Result.builder(useAtomValue(userAtom))
.onInitial(() => <div>Loading...</div>)
.onErrorTag('NotFoundError', () => <div>User not found</div>)
.onError(error => <div>Error: {error.message}</div>)
.onSuccess(user => <div>Hello, {user.name}</div>)
.render();
}
SubscriptionRef
SubscriptionRef<A> 是一个可变引用,其 .changes 流始终先将当前值作为元素 0 发出,然后发出所有后续变更。
其实现如下(来自 effect/src/internal/subscriptionRef.ts):
stream.concat(stream.make(currentValue), stream.fromPubSub(pubsub))
Ref.get 和 pubsub 订阅会在信号量保护下原子执行,不会遗漏任何事件。
// WRONG — prepended get is always redundant
Stream.concat(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
Stream.concat(Stream.make(yield* SubscriptionRef.get(ref)), ref.changes)
Stream.merge(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
// CORRECT — .changes already provides the snapshot
ref.changes.pipe(...)
若要跳过初始快照(例如,避免激活时出现多余的刷新),请使用 Stream.drop(1)。
分组
Object.groupBy 的类型为 Partial<Record<K, V[]>>,因此每个消费者都必须过滤 undefined 或提供默认值。Arr.groupBy 返回 Record<K, NonEmptyArray<V>>,是全量类型,因此 Object.entries 无需守卫。
import * as Arr from 'effect/Array';
const grouped = Arr.groupBy(ids, id => id.slice(0, 3)); // Record<string, NonEmptyArray<string>>
Object.entries(grouped).map(([prefix, group]) => query(prefix, group));
// vs Object.groupBy, where the same line needs:
// .filter((entry): entry is [string, string[]] => isNotUndefined(entry[1]))
仅当 Partial 会影响逻辑时才使用 Object.groupBy——例如,对不存在的键进行解构并提供默认值:const { deploys = [], deleted = [] } = ...。
反模式(禁止)
上方的 DON'T 列列出了各个禁止模式。references/anti-patterns.md
包含完整列表,每项都附有原因和正确替代方案。
空值守卫:与声明的类型匹配
在 effect 包中禁止使用 == null / != null(eslint.config.mjs 中的 noNullCompare)。
根据声明类型实际允许的值,选择 effect/Predicate 守卫——每种联合类型对应一个守卫,不得例外:
| 声明类型 | 守卫 |
|---|---|
T | undefined | isUndefined / isNotUndefined |
T | null | isNull / isNotNull |
T | null | undefined | isNullable / isNotNullable |
import * as Schema from 'effect/Schema';
import { isNotNull, isNotUndefined, isNullable, isUndefined } from 'effect/Predicate';
// T | undefined — the common case (Optional<T>, optional props, ?? sources)
if (isUndefined(maybeValue)) return;
Effect.filterOrFail(isNotUndefined, () => new NotFoundError({ message: '...' }));
// non-id string that must be non-blank — not `isNotUndefined && length > 0`
Effect.filterOrFail(Schema.is(Schema.NonEmptyString), () => new NotFoundError({ message: '...' }));
// AuthFields org id: `orgIdFrom` / `orgIdFromConnection` → `Option<OrgId>`; fail-if-missing via `Option.match`
// DefaultOrgInfo orgId: already `OrgId | undefined` (`Schema.optional(OrgId)` like cliId)
// T | null — e.g. RegExp.exec, JSON payload fields
const match = scriptRegex.exec(html);
if (isNotNull(match)) { … }
// T | null | undefined — e.g. `exclude?: vscode.GlobPattern | null` (optional AND nullable)
const arr = isNullable(exclude) ? undefined : [exclude];
运行时语义:isUndefined 等价于 x === undefined,isNull 等价于 x === null,isNullable 则是两者之一(node_modules/effect/src/Predicate.ts:611,649,925)。比类型所需范围更宽的守卫仍然可以通过编译——它只会暗示一种类型无法产生的情况,因此看起来像是在谎报值的情况。注意,typeof x === 'object' && x !== null 这种对象收窄应保持原样;这是结构检查,不是空值检查。
筛选器中的无点谓词
使用 Predicate.not() 取反筛选条件,兼顾无点风格与可读性。
import { not } from 'effect/Predicate';
// CORRECT — point-free negation
arr.filter(not(isFlowTest))
// AVOID — arrow wrapper around negation
arr.filter(x => !isFlowTest(x))
适用于任何谓词——内置谓词(isString、isError、isNotUndefined、isNotNullable)或自定义谓词。只有被包装的谓词可能依赖接收者:not(obj.method) 会分离接收者,因此请先确认实现没有使用 this(参见
composition-style)。
当谓词不是针对元素的谓词时,保留 !(!isCollectionType(decl.type)),除非已有命名的元素谓词,或值得新增一个。
导入:优先从 @effect/platform 深层导入
从 @effect/platform 通过聚合模块导入会打包 HttpApiSwagger(Swagger UI),而 esbuild 无法对其进行摇树优化。这会使 Web/桌面应用的每个输出包体积增加约 5.5MB,还可能触发安全扫描器的误报(例如 ClamAV 特征码)。
始终将子模块作为命名空间导入(子模块本身就是命名空间——它没有同名导出):
// CORRECT — deep namespace import, tree-shakes unused
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
// use: FetchHttpClient.layer
// WRONG — barrel import drags in HttpApiSwagger
import { FetchHttpClient } from '@effect/platform';
这与仓库中 import * as Effect from 'effect/Effect' 的风格一致。适用于所有 @effect/* 包。提交前请检查导入来源。
可观测性
// Structured logging
yield * Effect.log('Processing order', { orderId, userId, amount });
// Metrics
const orderCounter = Metric.counter('orders_processed');
yield * Metric.increment(orderCounter);
// Config with validation
const config = Config.all({
port: Config.integer('PORT').pipe(Config.withDefault(3000)),
apiKey: Config.secret('API_KEY'),
maxRetries: Config.integer('MAX_RETRIES').pipe(
Config.validate({ message: 'Must be positive', validation: n => n > 0 })
)
});
指标和追踪模式请参见 references/observability-patterns.md。
参考文件
如需了解详细模式,请查阅 references/ 目录中的以下参考文件:
composition-style.md-.pipe;用于单次使用的 yield 值的local/inline-single-use-yield-binding;终结运行器;无点安全性;用于副作用的 tap;Match 分发;守卫子句;线性主体使用无点管道还是生成器;对子序列进行恢复(从第一个 Effect 开始使用管道——不要嵌套Effect.gen)service-patterns.md- 服务定义、Effect.fn、Context.Tag 例外情况error-patterns.md- Schema.TaggedError、错误重映射、重试模式schema-patterns.md- 品牌类型、转换、Schema.Classlayer-patterns.md- 依赖组合、测试层anti-patterns.md- 禁用模式的完整列表diagnostics-findings.md- Effect LS 检查结果 → 修复方法,按规则分类observability-patterns.md- 日志记录、指标、配置模式