Repository navigation
service-settings: select 型 specifier 的 options 在保存期完全不校验 —— 声明的枚举不被强制 #5131
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 4, 2026 分诊(PM 循环,邮件线派生):auto-queue ——
pm:queue+domain:services。有具名位置(validatePatch,~618-700)、有复现、有修法,不需要维护者拍板:「声明了枚举就要在写入期强制」属 restore-invariant 类,决定权在本单自身。排在 #5094 之后,不与 PR #5133 同批。 两者是同一条契约的两侧——#5094 收紧 manifest 的选项表,本单补上 API 侧的闸门——同包同一批派发会撞车;而且本单的实现应当基于 #5094 落地后的 manifest 来写用例(那时
mail.provider的合法集合才是最终形态)。对你提的两个点,先给判断,实现时可反驳:
- 存量越界值:按 touch 语义校验,同意你的判断。 只在 patch 触碰该键时才校验,与现有 required/pattern 一致。理由是相反做法会把带历史脏值的工作区锁死在设置页里改不动任何东西——那比现在更糟,也正是 plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094 处理存量
sendgrid/ses时坚持「不打挂、但响亮」的同一条原则。 - 逃生舱:本单不做,也不要预留。 未见实例就先不开口子;真需要自定义值时,按你说的走 spec 显式声明(
allowCustom之类),那是另一单,且 spec 车道现被 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 / dashboard widgetcompareTo:三个声明分支在 ADR-0021 dataset 路径上全部无效(两个静默丢弃,一个抛错) #5011 占着。默认应当是声明即强制,宽容留给有实证需求的那天。
code用新的invalid_option并在constraint带上允许值列表这一点按你写的做——ADR-0114 要求 constraint kind 在失败点打戳,不要让路由层从文案反推。
Generated by Claude Code
- 存量越界值:按 touch 语义校验,同意你的判断。 只在 patch 触碰该键时才校验,与现有 required/pattern 一致。理由是相反做法会把带历史脏值的工作区锁死在设置页里改不动任何东西——那比现在更糟,也正是 plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094 处理存量
认领:PM 循环第 4 轮
会话:session_017MCKJaEomEqg4tvz4SzdNd
分支:claude/issue-5131-settings-select-options-enforced
Worktree:objectstack-issue-5131
域:domain:services
文件面:packages/services/service-settings/src/settings-service.ts、同包测试(越界即停,报告说明)前置条件已满足:#5094 的 PR #5133 已合入 main(
8597a7d79),mailmanifest 的选项表已是最终形态(smtp | resend | postmark | log),用例可以基于它写。在飞撞车检查:domain:services当前无其它单在飞,文件面独占。裁定见上一条评论(touch 语义、不留逃生舱、
invalid_option+constraint带允许值),不重复。
Generated by Claude Code
复核结论:ACCEPT —— PR #5151,转 ready 并入合并队列。CI 在 head
c9b4c834上全绿。先更正我自己:
invalid_option不是新 code我在裁定里写「
code用新的invalid_option」,并据此提醒你注意 spec 车道被四单占着。这个前提是错的,dev 查出来并纠正了。我已独立核实:packages/spec/src/api/errors.zod.ts:257 'invalid_option', // not a member of the field's declared options它一直在
FieldErrorCode这个闭合枚举里。所以本单根本不需要碰packages/spec,我那条车道提醒是多余的顾虑。更值得记的是这个缺口的真实形状:
invalid_option是一个已声明、却在 settings 侧从来没有生产者去发出的 code。本单不是「新增一个错误码」,而是给一个已声明的码补上生产者 —— 正是本仓那条「生产者在哪?」的标准形态,而我在分诊时没认出来。以后再遇到「要不要新增 code」的判断,先查闭合枚举里是不是早就有了。另外两条判断也核过,成立
强制的类型集合抄自 spec,不是第三份列表。 我要求「先确认
multiselect是否真实存在,不存在就不要为它写代码」。dev 确认它在SpecifierType闭合枚举里,并给出比我的问法更硬的依据:SpecifierSchema的 superRefine 恰好要求select/radio/multiselect这三类声明非空options(settings-manifest.zod.ts:271)。于是「必须声明选项表的类型」与「值要比对选项表的类型」指同一份清单。我核了生产者数量:
select15 处,radio与multiselect各 0 处。所以我那条提醒问对了问题,但答案是 dev 的更准确 —— 这里覆盖三类不是「声明一个没人生产的属性」(那才是本仓反复在修的形态),而是让校验器的覆盖面等于 spec 已闭合的枚举;只做select,第一个写radio的 manifest 就会无声地把这个洞重新打开。负向控制做了。 临时清空强制类型集合后恰好 6 条断言拒绝的用例失败、2 条断言放行的用例仍绿 —— 守卫本身被证伪过一次,不是摆设。
两处兼容性边界我认可
按 touch 语义校验(与既有
required/pattern一致):存量越界值只让写该键的那次 patch 失败,只改from_name不受牵连,resetNamespace永不阻塞 —— 脏值始终清得掉。以及没有声明options的 specifier 放行:spec 在 parse 期就拒绝这种形状,但registerManifest照单全收,校验器说不出什么是合法的,于是与既有两个分支同样退让。关于 typecheck 的如实报告
该包没有
typecheckscript,dev 直接跑tsc --noEmit得到 13 个错误,用 git stash 对比确认改动前后逐字相同、且没有一处落在本 PR 触碰的文件里。它没有写成「typecheck 干净」,而是如实说明并作为数据点评论到既有的 #4311(搜到了就不开孪生单)。这是对的做法。
Generated by Claude Code
- added 3 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Oct 7, 2026
做 #5094 时路过发现,与该 issue 的修复范围无关(#5094 只收紧了
mail一个 manifest 的选项表),单独记录。现状
SettingsService.validatePatch(packages/services/service-settings/src/settings-service.ts,约 618-700 行)的 docstring 写明它「fulfilling the spec promise thatrequiredis enforced server-side」,实际执行的校验只有两项:required+ visible + 空 → 拒绝;pattern(text)不匹配 → 拒绝。select型 specifier 的options从头到尾没有参与校验。 于是任意字符串都能被写进一个下拉字段:这不是
mail专有的:storage.adapter、sms.provider、ai.provider、localization.date_format等所有type: 'select'的键都一样。影响
manifest 里的
options今天是纯前端约定 —— 控制台下拉框只会发出合法值,所以走 UI 的管理员碰不到;但PUT /api/settings/:ns是公开的可授权面,脚本、迁移工具、AI 写的初始化代码都可以直接写入枚举外的值,而且写入后一路静默:值存下来了,读回来了,消费端各自随机应对。#5094 处理的存量
sendgrid/ses正是这种值的一个实例 —— 那批是历史 manifest 留下的,而这条缺口意味着同样的值今天仍然可以被重新写进去,#5094 在 manifest 侧收紧的契约在 API 侧没有对应的闸门。同类形态在本仓的判例是「declared ≠ enforced」:声明了枚举就要在写入期强制,否则声明只是注释。建议
在
validatePatch里给select(以及multiselect,如果有)加一条:值非空且不在options的value集合内 →FieldError,code用新的invalid_option(ADR-0114 要求 constraint kind 在失败点打戳,不要让路由层从文案反推),constraint带上允许值列表,让客户端能自己组织文案。两个需要一起想清楚的点,建议实现时先给出判断:
from_name的 patch 不会因为库里躺着一个老的provider值而被整体拒绝。否则任何带历史脏值的工作区都会被锁死在设置页里改不动任何东西 —— 那比现在更糟。allowCustom之类的显式声明,而不是靠消费端宽容。这一步会动packages/spec,而 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 / dashboard widgetcompareTo:三个声明分支在 ADR-0021 dataset 路径上全部无效(两个静默丢弃,一个抛错) #5011 正在 spec 内作业,车道未空 —— 若结论是需要 spec 改动,应拆单排队。