跳到正文

Blog

Appaloft 里的 TypeScript DDD(四):Specification

Appaloft 在 TypeScript 里轻量使用 Specification,包括 selection spec、readiness gate、policy decision 和 drift report。

AppaloftDDDSpecificationTypeScript

Specification pattern 的例子常常从可复用 predicate object 开始。Appaloft 大多数时候用得更轻:用命名的 spec 表达 selection、readiness、compatibility 或 policy decision,但不执行副作用。

最后这点最重要。Specification 可以说要选什么、要求是否满足、缺什么 evidence。它不应该悄悄 mutate deployment、创建 provider resource,或者修复 drift。

Selection spec 代替模糊查询参数

Installed application repository 使用命名 selection spec。Single selection 可以按 application id,也可以按 idempotency key;many selection 可以按 marketplace slug,也可以是 all。

这不复杂,但很有用。它避免了那种有十个 optional field、优先级全靠约定的 TypeScript query object。

当 repository 收到 { kind: "idempotency-key", idempotencyKey },意图是清楚的。当它收到 { kind: "marketplace-slug", marketplaceSlug },调用方不是在依赖 nullable applicationId 加另一个 marketplaceSlug 的隐式约定。

小 spec 会让测试更好读,也更不容易误扩大查询范围。

Repository 收到的不是一袋 loose optional filters,而是一个被命名的 selection:

export type CloudInstalledApplicationSingleSelectionSpec =
  | { readonly kind: "application-id"; readonly applicationId: string }
  | { readonly kind: "idempotency-key"; readonly idempotencyKey: string };

export type CloudInstalledApplicationManySelectionSpec =
  | { readonly kind: "marketplace-slug"; readonly marketplaceSlug: string }
  | { readonly kind: "all" };

Readiness spec 把 gate 说出来

Dependency provisioning 也有 specification 风格的输入。Plan 可以包含 version requirement、capability、readiness requirement、provider key 和 mode。Workflow 再推导 required acknowledgement 与 readback check。

重点是:unsupported capability 或缺 provider support 不应该藏在 provider exception 里。系统可以返回 adapter_unsupported、production apply gate blocked decision,或者带明确 evidence 的 status/readback shape。

比如 production external apply gate 会返回 blocked decision,列出 owner-approved runbook、provider readback evidence、secret-ref evidence、rollback-delete evidence 等 required evidence。这个 decision 不创建资源,它只是表达 policy。

这个 policy 返回的是调用方可以渲染或审计的数据,而不是一个半创建的 provider resource:

export function evaluateCloudDependencyProductionExternalApplyGate(input: {
  readonly kind: CloudDependencyProvisioningPlanInput["kind"];
  readonly providerKey: string;
}): CloudDependencyProductionExternalApplyGateDecision {
  return {
    schemaVersion: "appaloft.cloud.dependency-provisioning.external-apply-gate/v1",
    status: "blocked_on_owner_approval",
    kind: normalizeKind(input.kind),
    providerKey: input.providerKey,
    createsExternalResources: false,
    requiredEvidence: [
      "owner-approved-provider-runbook",
      "provider-readback-evidence",
      "secret-ref-evidence",
      "rollback-delete-evidence",
    ],
    reason: "paid_or_production_provider_apply_requires_owner_approval",
  };
}

Drift report 是 specification,不是 command

docs/cloud/code-as-infra.md 里有一条很容易在自动化里丢掉的线:drift detection 可以比较 observed state 和 target evidence,但 repair 必须走 explicit command。

这就是 specification 思路。Drift report 可以描述 finding、hash value、总结缺失资源、说明 observed state 和 portable spec 哪里不同。它不能变成隐藏的 command handler。

对 deployment control plane 来说这尤其重要。如果 projection 或 report 可以 mutate 基础设施,operator 就很难把 plan、acceptance、apply、verify 和 rollback 当作几个可 review 的时刻。

Specification 可以是普通数据

Appaloft 不要求每个 specification 都写成带 isSatisfiedBy 的 class。TypeScript discriminated union 和 readonly input object 往往更合适。

Spec 有价值,是因为它命名了一个意图:

  • 用 idempotency key 选择这个 installed application
  • 列出某个 marketplace slug 对应的 installed applications
  • 要求某个 dependency capability
  • 在 evidence 不足时阻止 provider apply
  • 把 observed runtime state 和 portable spec 做比较

实现可以是 function、class、repository method 或 workflow step。DDD 的部分,是被命名的意图和副作用边界。

失败模式

失败模式是把所有参数对象都叫 specification,最后什么也没变好。Appaloft 会尽量把这个模式留给歧义代价高的地方:repository selection、readiness gate、provider support、drift detection 和 policy check。

这样用时,Specification 不太像抽象炫技,更像一种运维诚实。它告诉系统知道什么、不知道什么,以及在改变世界之前还缺什么。