mcpField
E
effect-best-practices

effect-best-practices

github.com/forcedotcom开发1.0k41 次安装今天更新
代码仓库

关于 effect-best-practices

effect-best-practices 是面向使用 Effect 构建 TypeScript 应用团队的编码技能,提供服务、错误、组合、Schema 和资源生命周期方面的约定与诊断流程。

主要功能

  1. 01Effect 服务与依赖的指导
  2. 02类型化错误定义和处理模式
  3. 03Schema 与品牌 ID 约定
  4. 04Effect.fn 与组合方式指导
  5. 05通过命令行运行语言服务器诊断
  6. 06资源作用域与清理实践

适用场景

  1. 01按项目约定审查 Effect 代码
  2. 02在 TypeScript 编辑后运行诊断
  3. 03选择类型化错误和服务模式
  4. 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.md
composition-style.md
diagnostics-findings.md
error-patterns.md
layer-patterns.md
observability-patterns.md
schema-patterns.md
service-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 钩子会在每次 .ts Edit/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;吞掉错误;“以防万一”而捕获
IDSalesforce 记录/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 | undefinedisUndefined / isNotUndefined
T | nullisNull / isNotNull
T | null | undefinedisNullable / 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.Class
  • layer-patterns.md - 依赖组合、测试层
  • anti-patterns.md - 禁用模式的完整列表
  • diagnostics-findings.md - Effect LS 检查结果 → 修复方法,按规则分类
  • observability-patterns.md - 日志记录、指标、配置模式

评论

支持 Markdown