Skip to content

feat(objectql): 悬空引用巡检纳入 readonly 溯源族,单独分桶为 provenance (#4743 事实二) - #5719

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4743-provenance-audit-bucket
Aug 6, 2026
Merged

feat(objectql): 悬空引用巡检纳入 readonly 溯源族,单独分桶为 provenance (#4743 事实二)#5719
os-zhuang merged 1 commit into
mainfrom
claude/issue-4743-provenance-audit-bucket

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #4743 —— 仅事实二(巡检跳过收窄)。⛔ 事实一(engine.tsassertReferencesResolve 的过期注释)未做,#5504 在飞同文件,所以这里写 Part of 而非 Fixes,issue 不随本 PR 关闭。

前提核验(动手前对 origin/main 逐条确认)

issue 是线索不是规格,三条前提都在 origin/main(39e43c8)上核过:

前提结论证据
整体跳过仍在✅ 成立dangling-reference-audit.ts:231if ((def as { readonly?: unknown })?.readonly === true) continue;
#4556 的 NULL 化已落✅ 成立packages/metadata/src/loaders/database-loader.ts:585recorded_by: historyRecord.recordedBy,?? 'system' 已不存在;recordedBy 一路 optional 传入,无上游兜底
被豁免的只剩溯源族✅ 成立registry.tsAUDIT_FIELD_DEFS(created_by/updated_bylookup → sys_user, readonly: true)+ organization_id(lookup → sys_organization, readonly: true);owner_idreadonly: false,本来就在巡检里

改了什么(按 PM 裁决取方向 B)

readonly 不再是「丢弃字段」的理由,而是「findings 归哪个桶」的理由。

新增两个报告键,形状与该文件既有分组一致:

含义
provenance: DanglingReference[]readonly 溯源引用解析不到行 —— 样本行形状与 dangling 完全一致
provenanceUndetermined: number溯源引用的目标压根探测不了

两个键都是追加的、类型上 optional,与 aborted 同一处理、同一理由(手写的 report 测试替身仍然满足类型);本模块产出的每一份报告都显式赋值。dangling 的语义一字未动。

三个判断,每个都有它必须那样的理由

1. 为什么另起一桶而不是同桶(C):「引用了一个被删掉的用户」和「模型声明的外键断了」补救方式不同(通常无事可做 vs. 补种目标行或清链接),混在一起会让后者被前者淹没 —— 这正是该文件「Unknown and absent are DIFFERENT answers」一节最在意的失效模式。

2. 为什么 provenance 单独存在不触发 summary warn:在任何有年头的库上它每次健康运行都非空。每次都响的告警就是 #4747 刚花代价拆掉的坏告警,会把读者训练到跳过这一行 —— 而下一次 dangling 真有东西时也一起被跳过了。所以:计数随行(warn 因真实 finding 触发时把两个计数一起带上),明细常驻(返回的报告永远带全量行,给主动来问的调用方)。

3. 为什么 provenanceUndetermined 不并进 undetermined:undetermined会触发 warn 的桶。在一个不注册 sys_user 的栈上,每一个溯源值都探测为「说不准」—— 那是关于「哪些平台表被挂载」的事实,不是关于被审计数据的事实,并进去等于每次运行都为错误的事情拉响警报。这一条不是假想:现存的 engine-dangling-reference-audit.test.tsexpect(out.undetermined).toBe(0/1) 两处断言,正是靠这个分离才保持原义为真。

一处必要的连带改动:扫描顺序加了第三档

纳入溯源族意味着几乎每个对象都有可审计字段(applySystemFields 到处注入 created_by),于是有限的行预算会开始花在只带溯源的表上,而预算本来是为业务发现建的。prioritise 因此从两档变三档:安全面 → 带业务引用的对象 → 仅溯源的剩余。理由与 SECURITY_SURFACE_OBJECTS 完全同构,只是低一档:预算有限时,顺序决定哪个问题真的被回答。

反向验证(方向在跑之前就已预测:)

把整体跳过临时改回去(if (readonly) continue;),9 条新用例 8 条转红:

× a dangling `created_by` is reported — in `provenance`, not in `dangling`
× the two buckets never merge — a broken FK and a deleted user are filed apart
× an unprobeable provenance target counts in `provenanceUndetermined`, never in `undetermined`
× a table whose ONLY reference is provenance is now read — it used to be skipped whole
× provenance-only tables are scanned LAST, so a finite budget still answers the business question
× provenance ALONE does not raise the summary warning
× …but rides along in the payload once the line fires for a real finding
× [engine] a readonly lookup that resolves to nothing lands in `provenance`, not `dangling`
Tests 8 failed | 27 passed (35)

如实报告剩下的 1 条:a run with nothing to say still states both provenance buckets explicitly 两边都绿 —— 它钉的是「键必须显式出现」这一结构性事实(aborted: false 的同构用例),按定义不会随行为翻转。另有一条 a resolvable created_by is not reported at all 原本也两边绿,已加 expect(port.probes).toContain('sys_user usr_alive') 使其转红 —— 沉默必须是「查过且解析成功」,不能是「没人看」。

fixture 处置:整条替换,而非改写

a READONLY reference field is not audited 用例被整条退休,不是重新拼写。原因写在代码注释里:它的 expect(out.dangling).toEqual([]) 在新行为下依然通过 —— finding 只是挪了一个桶,不是消失了。留着它就是一条「因为什么都没产出所以绿」的用例。替换它的是断言「值到底去了哪里」的版本,那才是唯一能转红的写法。同一处置也应用在 engine-dangling-reference-audit.test.ts 的哨兵用例上。

消费方核验(按巡检的消费半径扫,不按被改包扫)

逐个核过,新增键均安全:

消费方读法结论
engine.tsinspectDanglingReferences原样返回追加键透明 ✅ 未改动
lifecycle/lifecycle-service.ts整体赋给 report.danglingReferences追加键透明 ✅ 未改动
lifecycle/lifecycle-service.test.ts手写 report 字面量作测试替身正是新键必须 optional 的原因 —— 必填会让这些字面量类型报错;55 tests 全绿 ✅ 未改动
packages/clischema-migrate.teardown.integration.test.ts结构性子集 { unreadableObjects, aborted? }追加键透明,1 test 绿 ✅ 未改动
index.ts 类型导出复用既有 DanglingReference 作样本行类型无需新增导出 ✅ 未改动
packages/climigrate/value-shapes.ts名字撞车但读的是 scan-value-shapes 的另一份报告无关 ✅

无 snapshot 钉住该报告形状(已全仓查过)。

顺带发现(未在本 PR 修)

验证

pnpm --filter @objectstack/objectql test → Test Files 121 passed (121) / Tests 1978 passed (1978)
pnpm --filter @objectstack/objectql typecheck → tsc --noEmit,无输出,exit 0
pnpm --filter @objectstack/cli test src/utils/schema-migrate.teardown.integration.test.ts
→ Test Files 1 passed / Tests 1 passed
pnpm check:query-options-erasure → ratchet holds: 84 unswept non-test site(s) in 19 file(s), none new
node scripts/check-nul-bytes.mjs → OK (scanned 5608 tracked text file(s); no raw ASCII control bytes)

已加 changeset(@objectstack/objectql: minor —— 报告形状对消费方可见)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx


Generated by Claude Code

… bucket (#4743)
The dangling-reference audit dropped every `readonly` reference field before
reading a row. That skip had two grounds and #4556 removed one of them: the
platform no longer writes a non-id into a reference column (the
`sys_metadata_history.recorded_by` sentinel is NULL now). What the skip still
covered was one family — the audit-provenance fields `created_by` /
`updated_by` / `organization_id` injected by `applySystemFields` — and those
hold genuine ids that genuinely dangle when a user or organization is deleted.
They are audited now, and filed apart from `dangling`: a deleted actor and a
broken business foreign key are different findings with different remedies.
Two additive, optional report keys carry the new class, mirroring the
unknown/absent split the report already makes: `provenance` (rows, same shape
as `dangling`) and `provenanceUndetermined` (probes that could not run).
`provenance` alone does not raise the summary warning — on an aged database it
is non-empty on every healthy run, which is the #4747 broken alarm. The counts
ride along whenever the line fires for a real finding.
Scan order gained a third tier (security surface, business references,
provenance-only) so a finite row budget still reaches the business findings it
was built for, now that nearly every object has an auditable field.
Part of #4743 (fact 2). engine.ts (fact 1) deliberately untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
@vercel

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 2:03am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude