Skip to content

docs(protocol): 按 SettingsService 真实契约重写 config-resolution(#5888) - #6031

Queued
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5888-config-resolution-phantom-api
Queued

docs(protocol): 按 SettingsService 真实契约重写 config-resolution(#5888)#6031
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5888-config-resolution-phantom-api

Conversation

@hotlong

@hotlong hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5888

结论先行

取证结果指向 issue 正文的处置 2(能力换了形状),而不是处置 1(能力从未落地):

  • 页面教的那套 context.config.* API 零实现;
  • 但这套分层配置能力真实存在,叫 SettingsService(@objectstack/service-settings,ADR-0007),存储是 sys_setting / sys_secret,对外是 /api/settings

所以本 PR 是按真实签名重写,不是删页。页面本身必须保留:content/docs/protocol/kernel/meta.json 收录它,protocol/kernel/index.mdx 链它两次,plugin-spec.mdx 结尾链它一次。

改动量:269 + / 810 −,单文件。这是一次近乎全量的重写,请按此预期审阅。

取证过程(逐符号对 origin/main 验存)

扫描面 packages/ apps/ examples/*.ts / *.tsx

阳性对照(证伪「扫描器坏了」):同一扫描面对页面自己点名的 envKeyOf 命中 8 处(packages/services/service-settings/src/settings-service.types.ts:247 等),coerceEnvValue 命中 6 处(settings-service.ts:1255)。零命中因此成立。

零实现符号(含分诊补充的 5 处)

符号 原行号 命中
context.config(整个对象) 全页 0
setUserPreference() 129 / 224 / 754 0
setTenant() 132 / 192 / 534 0
admin.setTenantConfig() 500 / 505 / 749 0
objectstack_tenant_config 207 0
objectstack_user_preferences 234 0
objectstack_secrets 565 0
ConfigValidationError 656 0
aws-secrets-manager + 四家外部密钥托管 589–613 0(AwsKmsCryptoProvider 等在 contracts/crypto-provider.ts 只作为宿主自备的注释出现,仓库不含实现)
Zod .meta({ secret: true }) 547–557 0(真实是 specifier 的 encrypted)
视图 onSave 533 0

整页核对额外发现的幻影(超出 issue 与分诊清单)

  • 配置文件层不存在:CLI 的候选名只有 objectstack.config.{ts,js,mjs}(packages/cli/src/utils/config.ts:57-61)。.yml / .json 零命中,objectstack.config.{env}.ts / .production.tsNODE_ENV 选文件零命中。原页 264–271 / 319–378 / 762–798 整段据此删除。
  • defineStack() 不收那些键:ObjectStackDefinitionSchema 的顶层键是元数据集合(objects apps views datasources plugins server …),没有 database / http / features / stripe / secretsdefineStack 对未声明键是 warn + 丢弃(warnUnknownAuthoringKeys),原样照抄会静默失效。
  • plugins 形状相反:真实是 plugins: z.array(z.unknown()),不是 { enabled: [...] }
  • 数组语义写反了:原页「Arrays are replaced, NOT concat」;真实 COMPOSE_KEY_DISPOSITIONSplugins: 'concat',所有数组集合都是 concat。
  • 没有 deep merge:SettingsService.get() 取 cascade 链上第一个非空条目,不做任何合并。原页 402–476 整段是幻影。
  • 六层写错成五层:真实是 env > global > tenant > user > default。原页把 global 整层漏掉,却多出了不存在的 runtimefile

词表纠正(tenant_id → organization)

sys_setting 自身没有租户列,租户行靠 scope='tenant' 区分。全平台租户身份是 organization:session 携带 organizationId,引擎只在对象确实声明了 tenant_id 列时才据此打戳(packages/objectql/src/plugin.ts:798)。全仓声明 tenant_id 字段的对象只有一个 —— sys_audit_log,且它是 Field.lookup('sys_organization', …)

补充一点比 issue 更精确的表述:issue 说「tenant_id 不是任何现存表的列名」略微过头,它确实作为驱动层列存在一处;但实质成立 —— 它从不是 sys_setting 的列,且语义上就是 organization。

重写后的内容(全部可验)

SettingsServiceget / getNamespace / createClient / set / setMany / resetNamespace 真实签名;envKeyOf 命名规则与 coerceEnvValue 强制类型;#5204 的「越界 env 值被忽略而非修复,且不产生 cascade 条目」;sys_setting 行标识 (namespace, key, scope, user_id);encrypted specifier 经 ICryptoProvidersys_secretLocalCryptoProvider 用 AES-256-GCM 且生产环境 fail loud;/api/settings 四条真实路由;错误码表 SETTINGS_LOCKED / SETTINGS_UNKNOWN_KEY / SETTINGS_UNKNOWN_NAMESPACE

「scope 是声明出来的,不是调用方选的」 单列一节 —— 这正是 setTenant() / setUserPreference() 按当前架构不可能存在的结构性原因:setManyreg.scopes.get(key) 取 scope 落盘,调用方无从指定。

关于「planned」措辞的边界

按 PM 指示,⛔ 未写任何「未来将有」的路线图散文。未落地的能力收敛成页尾一个 4 条的现状说明(Outside this contract),每条陈述现状而非承诺。原页已有的 os config CLI「planned」告示保留同样处理。

防复发:给核心示例加了 os:check

页面中心的 manifest 示例加了 {/* os:check */} 标记,check:skill-examples 会把它抽出来对已构建的 spec 声明tsc --noEmit

反向验证(先定方向、预期 red):把 scope: 'tenant' 改成 scope: 'organization' 后重跑,门确实变红并点名:

content/docs/protocol/kernel/config-resolution.mdx:100:3
    error TS2322: Type '"organization"' is not assignable to type '"global" | "tenant" | "user"'.

已还原。标记是活的,不是惰性装饰 —— 这页以后再漂移会当场红。

自验(全部前台阻塞执行,重装后完整重跑)

检查 结果
check:nul-bytes(+ 自检) OK (scanned 5763 tracked text file(s))
控制字符自扫(超出门的盲区) 编辑文件零命中
check:doc-authoring ✓ 362 files clean
check:docs-audit-scope ✓ scope 与 content/docs/ 同步:178 篇
check:skill-examples ✅ 208 prose examples type-check,且本页 :92 确认被抽取
pnpm --filter @objectstack/docs build(= CI Build Docs) 通过

Build Docs 抓到一个轻量门看不见的真实缺陷:首版文件尾部混入了两行工具闭合标签,MDX 报 Unexpected closing slash。已修,并完整重跑至绿。

Changeset

按派发令走 docs-only 路线,不提交空 changeset;本 PR 不发布任何包,请验收时打 skip-changeset


Generated by Claude Code

…rvice contract (#5888)

The page taught a `context.config` API that has zero implementation:
`context.config` itself, `setTenant()`, `setUserPreference()`,
`admin.setTenantConfig()`, and the `objectstack_tenant_config` /
`objectstack_user_preferences` / `objectstack_secrets` tables all return
zero hits across packages/, apps/ and examples/. The whole-page audit found
the gap is wider than the three symbols the issue named: YAML/JSON config
files, NODE_ENV-selected config files, `defineStack({ database, http,
features, secrets })`, deep-merge semantics, and bundled external secret
managers are equally unimplemented.

The capability itself DOES exist, under a different shape: `SettingsService`
(@objectstack/service-settings, ADR-0007), backed by `sys_setting` /
`sys_secret` and served over /api/settings. So this is a rewrite onto the
real signatures rather than a deletion.

Corrections of record:
- Cascade is five layers (env > global > tenant > user > default), not six.
  `global` was missing entirely; `runtime` and `file` do not exist.
- First non-null layer wins. There is no deep merge and no array merge.
- Scope is DECLARED by the manifest, never chosen by the caller — which is
  why setTenant()/setUserPreference() cannot exist as written.
- `tenant_id` is not a column on `sys_setting`. Platform-wide the tenant
  identity is the organization: sessions carry `organizationId`, and where an
  object does declare `tenant_id` it is a lookup to `sys_organization`.
- Arrays concat on stack composition — the opposite of what the page claimed.

The central manifest example now carries an `{/* os:check */}` marker, so it
is type-checked against the built spec by check:skill-examples and cannot rot
silently the way this page did.

Refs #5888
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:14pm

Request Review

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation labels Aug 6, 2026
@hotlong hotlong added skip-changeset PR has no user-facing published change; bypasses the changeset gate documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Aug 6, 2026 — with Claude
@hotlong
hotlong marked this pull request as ready for review August 6, 2026 15:24
@hotlong
hotlong added this pull request to the merge queue Aug 6, 2026
Any commits made after this event will not be merged.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31115725289 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 16 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Any commits made after this event will not be merged.
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

队列管家原样重投(第 10 轮巡检)

本 PR 于 16:14:03Z 被踢出合并队列added_to_merge_queue 15:24:45Z → removed_from_merge_queue 16:14:03Z)。已取完整日志归档认签名,判定为基础设施抖动与本 PR 的 diff 无关

签名(run 31115725289CI / job Temporal Conformance (live PG + MySQL),job 92664772577):

Getting action download info
Failed to resolve action download info. Error: Service Unavailable
Retrying in 22.425 seconds
Failed to resolve action download info. Error: Service Unavailable
Retrying in 28.201 seconds
##[error]Service Unavailable
##[error]Failed to resolve action download info.

死在 Set up job零测试执行pnpm test 从未启动)。同窗口(15:20–16:02Z)另有 #6034#6010#6032#5999#6012 的多条 job 报逐字相同的串,跨 PR 同源 ⇒ 平台事件,非仓内配置错。

台账依据#5810 正文「跨仓通用」表第 1 行 —— GitHub Actions runner 丢失 / npm registry 5xx / 网络超时(基础设施抖动,与 diff 无关)已知环境抖动,原样重投

处置队列管家原样重投 —— 已重新 enable auto-merge。⛔ 未改代码、未切 ready/draft、未重跑(rerun 复用原合并 ref,对这类红无意义)。平台面 16:03Z 起已恢复(其后启动的 job Set up job 均 success),本次重投预期能正常构建。

让行判据:本 PR 最近 30 分钟内无车道 PM 处置评论(仅 merge-queue-triage workflow 15:40:26Z 的自动分诊评论)⇒ 让行不成立。


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31120590926 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • (没拿到 job 级信息,点上面的 run 链接看)

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 4 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 51 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@hotlong
hotlong added this pull request to the merge queue Aug 6, 2026
Any commits made after this event will not be merged.

Copy link
Copy Markdown
Contributor

⛔ 队列管家拦截(第 17 轮巡检)——abandoned 聚合假红族第 5 例,未重投

本 PR 于 ~20:45Z 被踢出合并队列(两读数判据:gh-readonly-queue/* 已无 pr-6031-* 分支,且 origin/main 仍是 9e3709a4(15:14:30Z)⇒ 出队且未落地)。已取完整日志归档认签名(original_length 80/80,⛔ 未看 tail)。

结论:与本 PR 的 diff 无关,但签名尚未经人工裁定 ⇒ ⛔ 不原样重投。

完整签名

run / job 结束 终报错
CI 31127637555Dogfood Regression Gate(步骤 Verify dogfood shard results 20:45:34Z dogfood matrix aggregate result: abandoned / dogfood-verify result: abandoned::error::Gate leg dogfood did not pass (result: abandoned)
Lint & Type Check 31127637554 20:44:04Z 零 job 级 failureESLint success、TypeScript Type Check cancelled)—— run 级 failure 属生命周期状态

该 CI run 内 **14 job:filter + Test Core (1..3/3) + 聚合 Test Core + Console Pin Gate + Dogfood Regression Gate (1/3) 全部 success,5 个 job 因队列重建 cancelled,唯一 job 级 failure 是聚合门禁本身。零测试失败

判读ci.yml 的聚合门禁白名单是 success|skipped|cancelled,队列重建丢弃分片后聚合读数为 abandoned,落进 *) 兜底判红 ⇒ 假红踢出。这是同一族的第 5 例#6010 17:32Z / #6012 18:54Z / #6013 19:06Z / #6042 20:13Z / 本 PR 20:45Z)。

台账依据#5810 正文四张签名台账均无该行mongodb-memory-server / Test Core 5000ms / 冷缓存 / 跨仓通用基础设施抖动 —— 都不是本签名)。台账 ⛔ 只有人工可升级,本座位已连续第四轮在锚点单提请。未裁定签名一律拦截(试点判据 2:零「新签名被原样重投」事故)⇒ 本轮不重挂 auto-merge。

⚠️ 与本 PR 16:16Z 那条原样重投的区别:那次签名是 Failed to resolve action download info / Service Unavailable(死在 Set up job、零测试执行),命中跨仓通用表「基础设施抖动」行,有台账依据;本次签名不同,无台账依据。

建议动作(归 PR 所属车道,本座位不碰代码):本 PR diff 无问题,等 abandoned 门禁缺口裁定(治理单 #6082 已转 needs-user-decision)或车道自行判断后重投;⛔ 重跑无效(rerun 复用原合并 ref)。

让行判据:本 PR 最近 30 分钟内无车道 PM 处置评论(最近一条为 19:29:10Z 的 merge-queue-triage workflow 自动评论)⇒ 让行不成立,本座位处置。

⛔ 本轮未合并、未切 ready/draft、未重投、未重跑、未 force、未改代码、未动认领。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants