npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@forumlayer/contracts

v0.1.0

Published

Canonical ForumLayer schemas, operation registry, and runtime validators

Readme

@forumlayer/contracts

Research use only under LicenseRef-ForumLayer-Research-Only-1.0; see LICENSE in this package. Commercial use, production deployment, and customer-facing services are not permitted.

schema 是唯一事实源。 TypeScript 类型由 pnpm codegen 从 schemas/*.json 生成, src/generated/ 下的文件不要手工编辑 —— 改类型请改 schema 再重跑。

为什么校验器比类型重要

TypeScript 类型只在编译期存在,且 json-schema-to-typescript 处理顶层 allOf 时 会退化成开放对象([k: string]: unknown)。跨越信任边界时以运行时校验为准:

import { assertValid, type EvidencePackV1 } from '@forumlayer/contracts';

const pack = assertValid<EvidencePackV1>('evidencePack', await res.json());

入站请求、出站响应、从存储读回的 pack —— 这三处必须校验。内部函数之间传递 已校验过的对象不必重复调用。

这些 schema 守住的是什么

不是"字段类型对不对",而是合法 JSON 能不能自相矛盾。例如:

  • insufficient_evidence 必须伴随 search_exhaustiveness.status = completed —— 这是全额扣点的机械判据,不能靠执行器自报
  • effect = DENY 时全部 allowed 必须为假
  • 内容已删除或社区转私密时 usable_for_new_claims 必须为 false
  • pack 体内不接受 settlement_status / task_points_charged —— 结算发生在首字节之后,发第一个字节时那个值还不存在
  • pack 体内不接受 lifecycle.status —— 当前服务状态的唯一事实源是 pack_registry
  • matched 的地域判定必须有支撑信号 —— 无 basis 的 matched 就是把 unknown 写成 matched

每一条都有对应的测试(test/invariants.test.ts),失败时能直接指回文档里那句"必须"。

operation registry —— 三通路的唯一权威

registry/operations.v1.json 是 REST / Remote MCP / stdio adapter 三条通路的唯一事实源 (ARCHITECTURE §10.3.5、ADR-001)。pnpm codegen 由它完全生成四份产物:

| 产物 | 用途 | |---|---| | src/generated/operations.ts | TS 常量、OperationId / ToolName / OperationCategory / PolicyDomain / PolicyPurpose / PolicyAction / DecisionPoint 类型、REGISTRY_DIGEST、GENERATOR_PROTOCOL_VERSION | | generated/openapi.v1.json | REST OpenAPI 3.1,schema 全部 bundle 进 components.schemas,无外链 | | generated/mcp-tools-list.v1.json | Remote MCP 的 tools/list 响应,inputSchema / outputSchema 自包含 | | generated/adapter-descriptors.v1.json | stdio adapter 的 REST 映射与幂等/成本元数据 |

几条不能绕的规矩:

  • operation_id 是无语义 opaque ID(op_0001),永不复用、永不物理删除。 废弃只置 deprecated_at / sunset_at。破坏性变更走新 tool 名 + 新 ID,不是改旧条目。

  • 新增 ID 必须先进 ledger:pnpm allocate:operation-id 显式追加,codegen 不会替你补 —— 让生成器改自己的输入等于给不可变性检查开后门。

  • policy_domain 决定这个 operation 走不走 PDP(ENTITLEMENT_MODEL §5.0、ADR-002 §10.3,2026-08-04 裁定): entitlement = PDP 判定 + scope;control_plane = 仅 scope,不过 PDP。 判据不是"感觉像控制面",而是:该 operation 的输入或输出中是否出现任何 artifact、artifact 衍生物、 或从外部内容导出的统计量 —— 出现即 entitlement,一个字段都算。 当前 research.cancel 与 usage.get 是 control_plane(owner 点名裁定),其余全部 entitlement。

    research.get 保守留在 entitlement —— §5.0 的判据是单向的(出现 artifact ⇒ 必须 entitlement, 不出现并不强制 control_plane),而硬约束 4 说"归错域按 entitlement 处理"。实现方不自行扩大豁免面。 ⚠️ 两者曾经共用 run-state.v1,那本身就是一条现行违规:run_state.v1 带 pack_id / revision, 而 contracts 自己认定 pack / community 的句柄也是 artifact 衍生物 ——「拿到句柄还要走 pack.get 才有正文」不改变它是句柄这件事,§5.0 的判据也没有「只有正文才算」的例外。owner 已裁定 cancel 是 控制面,所以要改的是 schema 不是域:cancel 现在返回专用的 run_cancel_result.v1 (schema_version + run_id 回显 + cancellation_accepted_at,没有任何 artifact 句柄), research.get 继续用 run_state.v1 并保留 entitlement 判定。 结算字段刻意不进取消回执:取消何时 settle 属于 ARCHITECTURE §10 尚未裁定的 cancel / disconnect 传播语义,写成必填等于替 owner 把「取消与结算原子完成」裁定掉, 写成可选则把「没算完」与「算完了没告诉你」变成同一种响应。结算经 research.get 读。

  • control_plane 的 policy_purpose / policy_action 表达为「字段缺席」,不是哨兵值。 null / "NOT_APPLICABLE" / 空对象都是错的:任何一次漏判空,那个值就会以真实判定输入的身份进 PDP, 而 registry 没有任何办法阻止它。字段缺席则查表必然拿不到东西,消费方按既有规则 fail-closed。 schema 层用 if/then 双向强制:entitlement ⇒ 两者必填;control_plane ⇒ 两者必须缺席。

  • purpose 与 action 是三个正交字段,不是一个(ADR-002 §1):operation_category(文档分组与用量报表,不参与判定)· policy_purpose(ENTITLEMENT_MODEL §3.2 词表)· policy_action(§3.1 词表)。 三者各自独立标注,不允许存在 operation_category → policy_purpose 的映射表。 policy_purpose 的唯一来源是 registry,服务端查表得到后注入 TrustedContext,绝不从入站 request 读取。

  • policy_action 是 decision_point → policy_action[] 的映射,不是平铺的上界集合(ADR-002 §1):

    "policy_action": { "PLAN": ["PROCESS", "QUOTE", "READ"], "PERSIST": ["PERSIST"], "COHORT": ["SERVE"] }

    判定点取值来自 ARCHITECTURE §3 的三个 ★ 判定点,按生命周期先后:PLAN(步骤 4)· PERSIST(步骤 9,2026-08-04 由 ADR-002 §10.2 裁定新增) · COHORT(步骤 10)。 字面量与 packages/pdp 的 PLAN_PDP / PERSIST_PDP / COHORT_PDP 一致;registry 不自造第四个判定点 —— 新增判定点是架构决策,要走 owner 裁定(PERSIST 就是这样加进来的)。 每个判定点允许哪些 action 照架构原文写死在 schema 里 (PLAN ⊆ {QUOTE, READ, PROCESS} · PERSIST = {PERSIST} · COHORT = {SERVE}), 所以"把一个 action 塞进架构没给它安排位置的判定点"在 schema 层就失败。

    registry-core.mjs 里还有第二份 ARCHITECTURE_DECISION_POINT_ACTIONS,与 schema 字面量逐值比对。 不是重复而是双录:词表镜像检查只保证 action 属于全局 policyAction 词表, 把 PERSIST 判定点的 items 改成 ["READ"] 它照样通过 —— 而判定点存在的意义恰恰是 "这个 action 只能在这个位置被判"。 平铺集合回答不了 PDP 真正要问的那个问题——"这个 operation 在这个判定点允许哪个 action"—— 映射责任会被推给调用方,不同服务各自过滤出不同结果,registry 的唯一事实源地位就被架空。 映射里没有的判定点 = registry 没有为该 operation 在该判定点授权任何 action,消费方必须 fail-closed。

  • 查表用 lookupOperation(operationId, decisionPoint)(或 lookupOperationByToolName), 一次原子产出三样共享同一个 operation_id 的东西:注入 TrustedContext 的 contextBinding、 PDP 在该判定点的 decisionBinding、handler 的 handlerBinding。只要判定期望,用 registryExpectationFor(operationId, decisionPoint)。

  • 控制面走另一个入口:lookupControlPlaneOperation(operationId)(或 …ByToolName), 它不吃 decisionPoint(控制面路径上没有判定点可选),也不给 decisionBinding —— 给它一个判定绑定就是在鼓励别人拿去判一次不存在的判定。

    两个入口互不相交:控制面 operation 在 lookupOperation() 下永远返回 undefined,反之亦然。 于是"我该不该过 PDP"不是调用方读一个布尔字段自行决定的,而是由"哪个入口查得到"回答。 contextBinding 是判别联合(policyDomain: 'entitlement' 才有 policyPurpose), 不是"purpose 可选" —— 后者少写一次判空就会把 undefined 当 purpose 传进 PDP。 安全默认见 policyDomainFor():缺失、非法取值、未知 ID 一律 entitlement(§5.0 硬约束 4,不得 fail-open)。 本包故意不提供任何"把所有判定点的 action union 起来"的便利函数 —— 那种函数存在的唯一用途就是制造 ADR-002 §1 要消灭的那个缺陷。 只比对 purpose 不构成"两条链路查的是同一个 operation"的证明:当前全部已发布 operation 的 policy_purpose 都是 PRODUCT_SERVING,purpose 相等是常态,所以三个 binding 都带 operation_id。

  • §5.0 的四条硬约束,每条都要有机器检查("缺一条这个维度就会变成绕过权利判定的后门"):

    | 硬约束 | 机器检查 | |---|---| | 1 编译期固定、绝不从入站 request 读、进 digest 与身份字段 | schema 必填 + policy_domain 进 publicationProjection 与 IDENTITY_FIELDS + checkPolicyFieldsAreNeverInbound():任何 operation 的 input schema 闭包里不得出现判定字段,也不得 $ref common 的四个判定词表。禁名按规范化后的完整名字比较(去掉所有非字母数字字符再小写),因此 policy_domain / policyDomain / Policy-Domain / POLICY_DOMAIN 一并禁掉 —— 真正做分流的 routePolicyDomain() 认的是 camelCase,只禁 snake_case 等于没禁。禁名分两类:判定值(policyDomain / policyPurpose / policyAction / policyDecisionPoint)与判定记录的选择器(operationId / toolName,它们是 lookupControlPlaneOperation*() 与 routePolicyDomain() 的连接键 —— 入参能声明它,客户端就能在走 A 路由时自称正在执行 B)。合法用途请用带前缀的名字(target_operation_id),规范化后不命中。入参闭包一律不支持 patternProperties:任意正则与"规范化后等于禁名"的无限名字集合是否相交,静态判不了(^Policy- 一个已知拼写都不匹配,却接受 Policy-Domain) | | 2 control_plane 不得返回任何 artifact 派生数据 | checkControlPlaneCarriesNoArtifactData(),三层,见下 | | 3 不豁免租户隔离、配额、审计、删除 | contracts 侧只能守住"scope 仍必填、幂等键仍强制、仍进发布物";租户隔离与配额的执行不在本包,由 PEP / PDP 那条线负责 | | 4 缺失或非法一律按 entitlement | schema 必填 + policyDomainOf() / policyDomainFor() 的安全默认(未知 ID 也返回 entitlement)+ 控制面查表只认严格相等的 control_plane |

    硬约束 2 的三层是独立的,缺一层就有洞:

    • 第 1 层(ADR 原文口径):控制面 operation 的 input 与 output 的 $ref 闭包里不得出现 ARTIFACT_SCHEMA_IDS(evidence / pack / community 三族及其 *-ref 句柄、delivery-manifest、 quote、run-submit-result、workspace-*)。原文只说 output,这里按 §5.0 的分类判据取严。
    • 第 2 层(更重要):闭包里每一份可达定义都必须在 CONTROL_PLANE_SAFE_SCHEMAS 里, 且规范化正文的 SHA-256 必须匹配。§5.0 真正担心的是"往一份已经判定为安全的 schema 里 加一个字段" —— 加字段既不新增 schema、也不新增 artifact $ref,第 1 层完全看不见它。 正文钉死之后,加一个字段就必须显式改这张表,于是这次分类判断一定会出现在 diff 与 review 里。
    • 第 3 层(判据本身):闭包里不得声明 artifact 句柄字段(pack_id / packId / community_id / artifact_id / evidence_id / revision …),覆盖嵌套、数组、组合与条件分支、const / enum 字面量,以及 required / dependentRequired / dependentSchemas 的触发键 —— 它们不描述 形状,但点名说了这个字段存在。第 2 层只能说"正文变了,请重新判一次",判断仍留在人脑里; 这一层把判断写成代码。比较用规范化后的完整名字而不是词根包含 —— 词根扫会误伤 config_revision 这类合法字段,而误报会逼人把检查关掉。控制面闭包不支持 patternProperties 与 propertyNames(字段名不是静态可枚举的)。 ⚠️ 光扫名字挡不住开放对象:{metadata:{type:"object"}} 里一个句柄名都没写,实例照样能带 {metadata:{pack_id:…}}。所以这一层还要求控制面闭包的每个实例位置都能自证拒绝未声明内容 —— 与硬约束 1 的入参侧共用同一份判据(collectInstancePaths() + provesClosed()), 不另写简化版:简化版会在 allOf / 条件分支 / unevaluatedProperties 上给出错误答案, 而错的方向是放行。 ⚠️ 即便如此,它仍是危险名字 + 结构封闭的检查,不是"无 artifact 数据"的完整语义证明: 语义仍由第 2 层的正文哈希与 review 承担。

    CONTROL_PLANE_SAFE_SCHEMAS 不得有死条目(测试钉死:它必须与控制面 operation 的实际可达面 一一相等)。一条没人用得到的"已判定安全"条目不是无害的库存,它是一扇开着的门 —— 将来谁 $ref 到它,三层检查都不会说话。run-state / run-completion 因此从安全清单移到 ARTIFACT_SCHEMA_IDS:它们带 pack 句柄,第 1 层应当用判据拒绝它们,而不是靠第 2 层的"你没登记"。 闭包按片段求(reachableRefs()),不是按文档:整份 common.v1 里有 artifactType 这类外部数据词表,放行整份等于给控制面输出开一扇 $ref common#/$defs/artifactType 的门。 同理,入站禁令也从入参节点出发 —— 否则入参只用了 common#/$defs/uuid 也会因为 common 里存在 policyDomain 而误报。

    分类口径写死在 CONTROL_PLANE_SAFE_SCHEMAS 的注释里:聚合的 Run 执行量、作业终态与 点数结算依据属于租户自己的作业 / 账务状态(owner 裁定 cancel 为控制面时明确纳入的范围)—— 但"属于账务状态"只说明它可以进控制面,不代表它必须进:run_completion.v1 同时带着 pack_id / revision,那一份因此整体不进控制面,取消回执改用不含句柄的专用 schema; 按社区、来源、artifact 类型等维度拆分的命中量、覆盖量或内容成员关系属于外部导出数据, 一律不得进入控制面 —— §5.0 的反例警告说得很清楚:usage.get 一旦返回"本月检索了哪些社区" 就立刻变回 entitlement。

  • 一个 operation 可以有多个成功分支(ADR-002 §2)。research.run 声明 200 → ok(同步返回 pack) 与 202 → accepted(async=true,envelope.run_ref.run_id 非空、result=null)。客户端凭 run_id 调 research.get,wire 中没有 poll_url。生成器与测试都不得假设"只有一个成功状态码"。

  • planned 不进任何生成物;deprecated 必须继续进 —— 一置废就消失等于物理删除。

  • registry_digest(JCS + SHA-256)是全链路唯一的同一性标识,握手、发布物、CI 断言用的都是它。

    它的定义是「整份发布物的身份」,不是「wire 校验语义的身份」(ADR-002 §8)。原先两层不一致 —— operation 层把 description 当可变字段不进摘要,schema 层却哈希整份正文(改 title 就变 digest)。 现在统一按前者:发布物的任何字节变化都改变 digest,客户端据此判断"要不要重新拉取", 而不是判断"语义有没有变"。

    | 变化 | digest | |---|---| | 缩进 / 键序 / 条目顺序 / scopes 与 action 的顺序 | 不变 | | planned 条目的任何变化 | 不变(它们不进任何生成物) | | description / display / 成功分支的 description | 变(它们确实出现在 OpenAPI 与 tools/list 里) | | registry_version、deprecated_at / sunset_at / replaced_by | 变(同上) | | 任一语义字段、成功分支集合、被引用 schema 的正文 | 变 | | 生成器产出逻辑(经 GENERATOR_PROTOCOL_VERSION) | 变 |

    GENERATOR_PROTOCOL_VERSION 参与摘要(ADR-002 §8):同一份 registry + schemas 下, 改变 OpenAPI 信封拼装、MCP union 生成或 descriptor 字段集合,产出的 wire 已经变了而输入一个字节没动。 改动任何影响生成物字节的生成器逻辑,必须 bump scripts/lib/registry-core.mjs 里的这个常量。

    Protocol 1 = 首次引入、尚未对外发布的那一版实现。首次冻结/发布之前还没有外部消费者拿它 做过握手或缓存身份,反复 bump 只会造出从未存在过的历史版本号;首次合入或发布之后一律递增。

    digest 与不可变性 gate 回答的是不同问题,不矛盾:gate 说"这个改动允不允许", digest 说"发布物变没变、要不要重新拉"。可变白名单(description · display · 成功分支的 description · sunset_at · replaced_by 的一次性补充)里的字段改动 gate 通过且 digest 必变,两条由测试同时钉死。

  • 不可变性 gate 按字段比较 rest,不整体比较(ADR-002 §8):身份是 method + path(含占位符名字)

    • 成功分支的 {status, envelope} 集合;分支 description 在可变白名单里。 整体比较会把明确可变的分支 description 判成"REST 绑定被改动",与 digest 的划分自相矛盾。
  • schema $id 必须唯一(ADR-002 §8):loadSchemas 递归读取整个 schemas 树(npm files 会把这棵树整体打包),缺 $id 或任意层级两个文件声明同一个 $id 一律抛错并报出两个文件名。 schemas 根与 .json 条目必须是真实目录/普通文件,软链与特殊文件 fail closed,避免文件系统 loader 与 merge-base git tree reader 得出两种结果。重复的 $id 会让"哪份正文进 digest / 进发布物"取决于文件排序。 scripts/codegen.mjs 复用同一个 loader,不维护第二份检查;嵌套 schema 生成同层级类型目录,两个来源若 映射到同一个输出路径,或 schema 撞到 index.ts / operations.ts 固定生成物,都在写文件前失败,不能静默覆盖。

  • pnpm check:registry 与目标分支的 merge-base 比较(不是 HEAD^),并且已接进 pnpm test —— 根 pnpm test 会跑它,该任务键关掉了 Turbo 缓存(结论依赖 git 状态,按 inputs 缓存会假绿)。 显式基线解析不到、浅克隆找不到 merge-base,一律 fail closed。端到端负例(同一次变更里既改 registry 又改 ledger)见 test/registry-gate.e2e.test.ts。

    ⚠️ CI 接线仍是外部前置条件(ADR-002 §7.6):仓库里还没有 .github/。引入 CI 时必须同时配置 PR 上 REGISTRY_BASE_REF="origin/$GITHUB_BASE_REF" + 足够的 checkout 历史, 并给主分支 push 一个明确基线(如 HEAD~1)—— 否则主分支会永远停在 merge-base === HEAD 的严格模式失败上。

scope 词表已由 owner 于 2026-08-04 签字(ADR-002 §9.1):原 research:execute 拆成 research:run 与 research:cancel —— 合成一条会让"只读 + 可取消"这种最常见的 Agent 授权配置 无法表达,取消是止损动作,通常应该比发起更容易获得。其余值照签字形态冻结。 quote:execute 与 usage:read 按 §9.1 本轮不随计价契约一起冻结:没有任何已发布条目引用它们, 改动只影响 planned 条目。 将来的 export / deletion / workspace write 等 scope 走新增而不是拆分现有值,因此不构成破坏性变更。

  • scope 词表是 registry-only 的,已从 common.v1 拆出到 schemas/operation-scope.v1.json(ADR-002 §8 §9.1)。 原先它寄居在 common.v1,而 common.v1 是 wire 闭包成员 —— 会被 bundleSchemas 整份嵌进 OpenAPI 的 components/schemas 与 MCP 的自包含 schema,也整份进 registry_digest 的 schema 闭包。 于是标着"本轮不冻结"的提案值照样出现在发布物里:文档说未发布、产物实际发布。 没有任何 wire schema 引用过 scope 词表,只有 operation-registry.v1.json 引用它,因此它独立成文件后 不进 generated wire/catalog JSON 的 schema bundle(OpenAPI components.schemas / tools/list 的自包含 schema), 也不进 digest 闭包 —— operation-scope.v1.json 不是 wire 根,也没被任何 wire 根 $ref。

    准确口径:它仍然参与本包自己的 schema→TS codegen(src/generated/operation-scope-v1.ts), 那是开发态类型,不是对外 wire 契约;文档根本身就是冻结词表,所以生成的类型只含那 6 个值。

    • operation-scope.v1.json 的文档根= 已签字冻结的 6 个值,只有它们可以被 active / deprecated 条目使用: 这条由 operation-registry.v1.json 的 lifecycle 条件(schema 层)+ checkPublishedScopesAreFrozen (不变式层)双重强制,不是注释。planned 条目要激活,必须先把它引用的 scope 加进冻结词表 —— 那是 owner 的显式冻结动作。⚠️ schema 能强制"没冻结就不能发布",强制不了"冻结的人真的拿到了签字", 后者靠 review 流程。
    • $defs/registryScopeName = 只校验命名形态、不冻结取值,planned 条目的提案值走它。提案可以未定,但笔误不行。
    • 已发布条目实际持有的 scope 值照常出现在发布物里 —— OpenAPI 的 x-scopes、tools/list 的 _meta.scopes、adapter descriptors。那是数据,该发布就发布;不发布的是尚未冻结的词表。
    • 对照:policyAction / policyPurpose 这类 ENTITLEMENT_MODEL 已定义的完整词表留在 common.v1, 进 digest 是可接受的,不要顺手一起拆走。判据是"这份词表是不是 wire 语义的一部分",不是"名字里有没有 policy"。
    • "未冻结"只约束发布,不代表 planned 条目是自由涂改区。 它引用的 scope 名仍然是身份字段, 受不可变性 gate 保护(scopes 在 SET_IDENTITY_FIELDS 里)。owner 批准同名 ⇒ 把值加进冻结词表、 lifecycle 改 active,scopes 一个字不动,gate 通过;owner 否决这个名字 ⇒ 只能按既有规则走 新 tool 名 + 新 operation_id,不能原地改名再激活。
    • 验收口径写清楚:未冻结的 scope 不进 generated wire/catalog 产物(generated/*.json、src/generated/) 与 digest 闭包。它们仍然存在于 registry/operations.v1.json 的 planned 条目里,而 registry/ 在 package.json 的 files 与 ./registry/* 导出里 —— 也就是说 npm 包仍然带着这份开发态 canonical registry 原文。那是 registry 的输入,不是 generated callable catalog;"没发布"指的是没被生成成 可调用的 operation、也没被冻结成对外契约,不是"这段文本不随包分发"。dist/ 同样在 files 里, 发布前必须重跑 pnpm codegen && pnpm build,否则陈旧构建产物会把已经拆掉的词表带出去。

计价相关的两个 operation 按 PRODUCT_PLAN D28 维持 planned:P1 前期免费, usage.quote 与 usage.get 作为独立入口不上线,usage.v1 的 wire shape(ADR-002 §7.2)不冻结。

⚠️ 但 quote.v1 已按 ADR-002 §10.4 解冻并正常冻结。 D28 的措辞据此修正: 暂缓的是货币价格契约,点数不在暂缓之列 —— 点数是 D23 定义的内部工作量单位, 对外露出它不等于开始收费;没有它 dry_run 基本没有实用价值,而 ARCHITECTURE §10.3.5 又要求高成本 tool 必须实现 dry_run。所以 quote.v1 照常出现在发布物里 (它是 research.run 的 dry_run 载荷,ADR-002 §7.4),解冻的是 schema,不是入口。

quote.v1 中不得出现任何货币、价格、折扣、税相关字段 —— 这条由 checkQuoteWireHasNoPricing() 机器强制,挂在不变式上(codegen / check:registry / vitest 三处都跑): 属性名走正向白名单(新增任何字段先失败一次,再显式判断它是不是货币口径), 货币词根扫描作为第二层兜住嵌套字段。只用关键词黑名单是不够的 —— rate / subtotal / unit_value 这类别名永远漏得掉。 ⚠️ 免费期不通过改 cost_class 实现。 cost_class 描述的是"这个 operation 本质上是否消耗点数", 是固有属性;免费期用额度授予实现。把 task_points 改成 free 会让恢复收费时的 cost_class 变更成为破坏性契约变更,并且免费期的全部用量记录会缺少可核对的点数口径。 research.run 因此仍然是 task_points。

发布阻断清单:合入与发布是两道门

pnpm test 全绿不等于 contracts 可发布。测试守的是"合法 JSON 不能自相矛盾", 守不了"这些语义有没有被 owner 拍板"。未决项记在 registry/release-blockers.v1.json, 由 pnpm check:release 机器强制:只要那份清单非空就失败。

它管的是发布,不是合入(ADR-002 §10.1 裁定):清单非空时可以 commit 并继续开发, 只是不得对外发布 —— 一道门会让整条开发线停在裁定上,而裁定往往要等真实数据。 CI 的形态与之对应:verify 每 PR 必过管合入,release-gate(仅 tag 与手动触发)管发布, publish job needs: release-gate。它因此故意不接进 pnpm test。

当前清单为空 —— ADR-002 §10 的七条裁定(2026-08-04)把原有四条全部消解:

| 原 id | 怎么消解的 | |---|---| | PERSIST_DECISION_POINT_UNRESOLVED | §10.2 裁定新增第三个判定点 Persist PDP(ARCHITECTURE §3 步骤 9),research.run / research.resume 据此声明 PERSIST:["PERSIST"] | | CANCEL_POLICY_DOMAIN_UNRESOLVED | §10.3 裁定新增 policy_domain 维度,research.cancel / usage.get 归 control_plane,不再硬标一个不存在的 cohort 判定 | | PRICING_WIRE_STILL_PUBLISHED | §10.4 裁定暂缓的是货币价格契约,点数不在暂缓之列;quote.v1 正常冻结,不是"仍然违规"而是"它本来就该在" | | SECTION_7_NOT_APPROVED | §10.5 三条领域语义(Run 结算 / community 双阈值 / 结构化版规三态)全部认可 |

⚠️ 空清单不等于"没有任何遗留风险",它只等于"没有等待 owner 拍板的事项"。 ADR-002 §7.8/§8 要求的已发布 $id 历史正文不可变门禁现已接入 check:registry: 它从目标分支 merge-base 读取 base registry 与 schemas,以 base 的 active/deprecated wire $ref 闭包为冻结范围,按 $id(不按路径)比较 canonical 正文。删除或原地改写 已发布 $id 会失败;base planned / 不可达 schema 与 current 新增主版本不受影响。

⚠️ 另一条遗留:registry/ 已进版本库,policy_domain / policy_purpose / policy_action 已受 merge-base gate 保护;但现有 gate 没有 owner-approved breaking change 的机器化出口。 下次再发生这类 owner 批准的判定输入变更之前,需要设计精确绑定 from/to 的审批或迁移机制, 不得临时加一个宽泛 waiver(而且审批记录必须先独立合入受保护基线,同一个 PR 里 自己写一条 approval 只能证明"有人写了 approved_by")。

policyPurpose / policyAction 是 ENTITLEMENT_MODEL 词表的镜像,不是第二个权威 —— packages/policy-schema 落地后必须改为由它生成或 $ref 并加一致性门禁(ADR-002 §7.0)。

active 条目的 input_schema_ref / output_schema_ref 必须解析到仓内真实 schema。 悬空 ref、占位 ref 在测试里直接失败 —— 这条是防止 registry 退化成一张漂亮的空表。

错误重试语义

ERROR_POLICY 是唯一权威。客户端不得靠 HTTP status 猜是否可重试。

retryable(重试可能成功)与 safeToRetry(重试不产生副作用)是两件事, 两者都为真才能自动重试;retryable 但不 safe 的必须带幂等键。