Repository navigation
sys_email.highlightFields names to, but the field is to_addresses — a dangling reference on a shipped platform object #15629
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 5, 2026 分诊 ·
pm:queue/domain:engine/priority:p2/bug⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
复核(
origin/main=a55efc6,带活对照)卡片断言 复核结果 highlightFields第 2 项是'to'✅ packages/platform-objects/src/audit/sys-email.object.ts:32—highlightFields: ['subject', 'to', 'status', 'sent_at'],对象上没有名为 to的字段✅ ^\s+to:= 0 命中收件人列其实叫 to_addresses✅ 同文件 :59—to_addresses: Field.text({其余三项都解析得到 ✅ 这就是那条活对照: subject::90、status::181、sent_at::208三个全部命中 ⇒ 上一行的零命中不是仪器坏了⇒ 四项里恰好一项悬空,且悬空的正是收件人列。卡片一字不差。
⚠️ 本轮本席位在别处刚吃过一次「零 + 死对照 = 无效读数」的亏,所以这里的零命中是和三个活对照同一次读取的。为什么锚定
domain:engine修复落在
packages/platform-objects/src/audit/sys-email.object.ts的一行。按车道表packages/platform-objects属domain:engine(该车道含packages/objectql、packages/core、packages/formula、packages/metadata*、packages/platform-objects、packages/drivers/driver-*)。⛔ 不是
domain:devx:packages/lint是发现它的工具,不是修复的落点——锚定规则看的是修复落在哪个包。卡片自己已经把这条边界划对了(#15495 的 PR 限定在packages/lint,本卡是packages/platform-objects的一行内容修复,自带 package 与 changeset)。priority:p2的理由两条后果,都真实,但都不是 p1:
- 界面少一列,且无声。
highlightFields驱动默认列表列、记录卡、预览与详情高亮条;每个消费者对解析不了的条目静默跳过 ⇒ 平台自己的邮件日志上,收件人那一列在这些面上不出现,而没有任何地方报错。 - 该对象现在过不了自己的写门。 自 [P0] Studio package publish accepts a dangling
highlightFieldsreference — the authoring gate never sees what the app builder produces #15254 把object-field-ref-unknown交叉到 object 写门之后,sys_email以当前形状无法经PUT /api/v1/meta/object或 package publish 重新发布(422INVALID_METADATA)。
不给 p1 的理由:它以 registry 对象形态出厂,启动不受影响,今天没有任何东西是坏的运行时。这是一个「谁通过门去编辑它谁就撞墙」的活陷阱,不是正在造成损害的故障。
不给 p3 的理由:修复是一行,而后果是平台自有对象上的一处可见缺列 + 一道自我封锁的发布门。让它留着的成本远高于修它。
交给接手者的三条
- 只改那一行。
['subject', 'to_addresses', 'status', 'sent_at']。⛔ 不要顺手重排highlightFields、不要顺手加列。 - 补一条防复发的判据,但别越界。 真正耐久的做法是让
object-field-ref-unknown对出厂对象语料成为红门(今天它只在写门上);⚠️ 那是一次门禁扩面,不在本卡的一行范围内 ⇒ 若判断值得,另开卡,⛔ 不要挂在本 PR 上。本 PR 内的合理增量最多是一条针对sys_email的断言。 - ⛔ 不要顺手处理
dc_account。 卡片在同一次 sweep 里记了第二个 finding(packages/qa/downstream-contract/src/account.object.ts未声明sharingModel⇒ 同一道门security-owd-unset报 error),并明说是低置信度、可能是最小契约 fixture 的有意为之,故意只记录不立卡。它落在packages/qa(domain:cli车道),与本卡不同包、不同车道 ⇒ 想推进请另开卡,⛔ 不要并进本 PR。
分诊席位 ·
claude-opus-5· 本轮 R+156
Generated by Claude Code
- 界面少一列,且无声。
Claimed by the PM dispatch loop.
Claim: session
session_01ARYe3yQTQCUFm5qPYNgKaJ, branchclaude/issue-15629-sys-email-highlightfields-to-addresses.
Clause-②: no.Card-specific — this is a ONE-LINE fix and the discipline is entirely about NOT doing more.
The change:
packages/platform-objects/src/audit/sys-email.object.ts—highlightFields: ['subject', 'to', 'status', 'sent_at']becomes['subject', 'to_addresses', 'status', 'sent_at']. That is the whole code deliverable.⛔ Only that line. Triage was explicit: ⛔ do not reorder
highlightFields, ⛔ do not add columns, ⛔ do not "tidy" the object while you are in there.⛔ Do NOT touch
dc_account. The same sweep recorded a second finding —packages/qa/downstream-contract/src/account.object.tsdeclares nosharingModel, so the same door raisessecurity-owd-unsetaterror— and the filer deliberately recorded it rather than filing it, flagging it as low confidence and plausibly intentional for a minimal contract fixture. It is a different package and a different lane (packages/qa⇒domain:cli). ⇒ If you think it should move, ⛔ not here: file a card.⛔ Do NOT widen
object-field-ref-unknowninto a corpus-wide gate. Triage named this as the tempting over-reach: the durable repair is making that rule red against the shipped-object corpus (today it only runs on the write door), but⚠️ that is a gate-surface expansion and is not in this card's one-line scope. ⭐ The reasonable in-PR increment is at most one assertion aboutsys_email. If the corpus gate is worth building, file a card.⭐ Triage's verification is worth copying, because it is the exact shape this loop demands: the zero-hit (
^\s+to:= 0) was taken in the same read as three live controls (subject::90,status::181,sent_at::208, all hitting). ⇒ The zero is a real absence, not a broken instrument.⚠️ Triage noted it had just been burned elsewhere that shift by 「零 + 死对照 = 无效读数」. Re-derive it the same way — and ⛔ re-derive the line numbers; this repo's files move daily.Two consequences, both real, and the reason it is p2 not p1:
- A silently missing column.
highlightFieldsdrives default list columns, record cards, previews and the detail highlight strip; every consumer silently skips an entry it cannot resolve ⇒ the recipient column is absent from those surfaces on the platform's own email log, with no error anywhere. - The object cannot be republished through its own write door. Since [P0] Studio package publish accepts a dangling
highlightFieldsreference — the authoring gate never sees what the app builder produces #15254 crossedobject-field-ref-unknownonto the object write door,sys_emailin this shape gets 422INVALID_METADATAfromPUT /api/v1/meta/objector a package publish.
⛔ Not p1, and the reason matters for how you frame the PR: it ships as a registry object, so boot is unaffected and nothing is broken at runtime today. It is a live trap for whoever edits it through a door rather than the file — ⛔ not an active failure.
⭐ Reproduce it through the real door, not by reading. The card's finding came from pushing the object through
runRuntimeAuthoringRules({ type: 'object' })with its siblings as context, and gotERR sys_email: object-field-ref-unknown objects.sys_email.highlightFields[1]. Drive that yourself before and after — a one-line fix still deserves a red-then-green.Clause-② reading (this seat's, not binding):
no— a content fix inside a shipped object declaration; no exported symbol or signature moves and no request changes disposition except the one that was wrongly refused.⚠️ Note the tension honestly:sys_emailcurrently fails its own write door and after the fix it passes — so on a literal reading of limb 2 something is newly accepted. ⭐ That is a false rejection being removed, the same shape this seat ruledpatch/noon earlier today. Re-derive it on@objectstack/platform-objectsand state both limbs separately.⚠️ ThisClause-②reading is this seat's and is NOT binding on you. Re-derive it from your own diff and re-declare it in your report. This seat got a tier call wrong in BOTH directions today (#14552 predicted a park that measurement retired; #14646 and #14935 declarednofor diffs that widened a published surface) — every one was caught only because the round re-measured.⚠️ Instrument limit, stated because two rounds burned time on it:api-surface/artifacts exist only forpackages/spec. For any other package the published surface is itsfiles[]+types(dist/**). The instrument that works: build at your head → swap the changed source file back toorigin/main→ rebuild → diff the builtdist/index.d.ts→ restore byte-exact and prove the restore (blob == HEAD blob ANDgit diff HEADempty). The test is 「does any exported symbol/signature move, or is any request newly accepted or rejected」 — ⛔ not 「did the signature line change」.✅ The Corepack CI defect is FIXED.
check-regen-pending.mjs --self-testreddenedorigin/main's own push build and six PRs today; PR #16002 landed asf50c394da(verified by content onorigin/main). If you cut from currentmainyou will not see it. If you DO seeLint & Repo Gatesred oncheck:merge-driverwith✗ self-test failed -- 1 failure(s) (cases and floor), your base predatesf50c394da— mergemainand re-run. ⛔ Do not skip, disable or quarantine anything over it.⭐ Discipline notes earned the hard way today, all from real failures:
- An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own
EXITtrap restored the file as it exited, and the suite then passed against the restored tree. Prove the mutation is on disk before measuring (anchored grep counts, orgit hash-object!= HEAD blob) and prove the restore after, in one shell. - A failure count is not a reading about success while anything is still running. Read
statusexplicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed. - A zero symbol count needs its comment filter run FIRST — code that documents an absence makes a bare count non-zero — and every zero-hit grep needs a firing positive control. A control aimed at a symbol that does not exist in the repo is not a control.
cancelledis notfailed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run islint.yml'sconcurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run'screated_at.- The shared verify lock is contended.
scripts/pm/os-verify-lock.shreturning exit 99 is a timeout = NO READING, ⛔ not a green and ⛔ not a red. Re-run to a real result and say so. Multiple rounds recorded 99s today (540s / 352s / 459s waits) with the holder alive and building normally — that is cost, not breakage. - Shared-ref hazard:
origin/mainmoves under you when a sibling worktree fetches. Pin every diff and merge to a sha, never to the ref name. One round read 9 changed files that were really 5.
⛔ Never edit
content/docs/releases/**in a code PR — release notes are written centrally at release time. Your PR's input to them is its changeset. E3 (2026-09-05) 「已发布必修,其余立卡」 — "published" is a MEASUREMENT against builtdist/**/*.d.ts,dist/**/*.{js,mjs,cjs}andcontent/docs/**, not a guess.⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never
git stash—refs/stashlives in the common.gitdir, so all worktrees share one LIFO stack and yourpoptakes another agent's entry while reporting success. Use a patch file or a wip commit. ⛔ Never force-push or rebase a pushed branch.Bump level: additive widening of a published surface ⇒ at least
minor. ⭐ The act wins over the commit type — the gate at.github/workflows/pr-automation.ymlonly refusesmajor, so it will not catch apatchthat should have beenminor.
Generated by Claude Code
- A silently missing column.
os-dev-report
{ "issue": 15629, "status": "done", "branch": "claude/issue-15629-sys-email-highlightfields-to-addresses", "pr": "https://github.com/objectstack-ai/objectstack/pull/16076", "premise_still_valid": true, "summary": "One line, exactly as ruled: packages/platform-objects/src/audit/sys-email.object.ts:32 highlightFields ['subject','to','status','sent_at'] becomes ['subject','to_addresses','status','sent_at']. Nothing reordered, no column added, nothing tidied; dc_account untouched; object-field-ref-unknown NOT widened to the corpus. Plus the one permitted increment: sys-email.highlight-fields-resolve.test.ts, scoped to sys_email, two cases (the pin and its anti-vacuity control), and a patch changeset. Branch merged origin/main c463d03e0 after dispatch-gates.mjs reported STALE TREE; final head b5ffe86e0, draft PR 16076. ALL FOUR ZONE-2 ASSUMPTIONS HELD, with two refinements worth PM's attention. (2.5) The instrument named in the dispatch word is BLIND for this package: dist/index.d.ts is an 11-line re-export barrel and is byte-identical across the ablation; the change surfaces only in dist/audit/index.d.ts. An agent diffing only the named file would have reported a confident 'no published-surface change'. (2.4a) Consumers do not uniformly 'silently skip': read in the objectui sibling, RelatedList.tsx prunes the unresolvable name and renders one column short, while deriveHighlightFields passes it through to record:highlights, and because sys_email sets nameField 'subject' the title is dropped first, so 'to' was the FIRST chip on the detail highlight strip. Both paths are silent, so the p2 call is unchanged. Clause-2 re-derived as 'no' with the tension stated in the PR body, and one limb-1 judgment left for PM below.", "tests": "RED-THEN-GREEN THROUGH THE REAL DOOR (Zone 2.3). runRuntimeAuthoringRules({type:'object'}) with the audit module's 9 siblings as context. Before, at base 5f4f1f6e2: exactly one error, severity error, rule object-field-ref-unknown, path objects.sys_email.highlightFields[1] (vitest exit 1). After: errors [] and advisories [], validateReferenceIntegrity among the 8 rules run (vitest exit 0). The pin's control case (old entry restored) refuses in BOTH runs, so the green is not vacuous. ANCHORS (Zone 2.1) re-derived at 5f4f1f6e2 and IDENTICAL to triage's at a55efc6: highlightFields :32, to_addresses :59, subject :90, status :181, sent_at :208. ZERO-HIT (Zone 2.2) taken in the SAME grep invocation as the three live controls: '^[[:space:]]+to:' = 0 while subject/status/sent_at each = 1. Instrument proven live. MERGED-HEAD UNION at b5ffe86e0 (rebuilt first: the merge moved packages/spec). platform-objects test 35 files / 527 tests passed, exit 0. platform-objects typecheck exit 0 -- BUT READ AS NOT MEASURED FOR THE NEW FILE: tsconfig.json excludes **/*.test.ts, and tsc --listFiles confirmed 0 test files in the program (control: the .object.ts source IS in, count 1). Measured instead with an ad-hoc test-inclusive program: 3 errors total, 0 in the new file. That ad-hoc run CAUGHT A REAL TS2677 in my first draft of the test, which the package's own typecheck reported green through; fixed in commit f449a6670. The 3 remaining are pre-existing in feature-gate-guard.test.ts and match the ledgered TEST_DEBT entry for this package exactly (errors: 3, TS2339 x2 + TS7006 x1), so the ratchet holds. DOWNSTREAM: plugin-email (the only package importing SysEmail) typecheck exit 0, test 29 files / 460 tests passed -- after building its 18-package closure; its first run's failures were unbuilt-dependency MODULE_NOT_FOUND, read as NOT MEASURED, not red. GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, provenance line checked (tree b5ffe86e0, --repo assertion holds, no staleness warning after the merge); 54 runnable invocations harvested with --commands, 51 GREEN, each exit code captured before any pipe and each quoting its own verdict line. 3 answered PREREQUISITE NOT MET, exit 3, read as NOT MEASURED and neither pass nor finding, all needing a full workspace build CI performs: check:dual-build-cjs-loads, check:i18n, check:type-check-debt. ONE INVALID READING, RE-TAKEN: check:dts-closure first returned exit 1 naming missing .d.ts in core and lint. Not a finding -- I had run the gate sweep concurrently with a rebuild of those packages in the SAME worktree, and tsup clears dist/ before writing. Re-run on a settled tree: exit 0, '82/82 declared declaration file(s) present across 17 package(s)'. Lesson for the seat: a gate read against a tree being rebuilt underneath it can go wrong in EITHER direction, and the wrong-green half would have been invisible. CLAUSE-2 ABLATION (Zone 2.5), on @objectstack/platform-objects. Legs: build at HEAD, hash dist/index.d.ts + dist/audit/index.d.ts; swap the source file back to origin/main; rebuild; hash; restore; rebuild. Mutation PROVEN on disk before measuring: blob c154aba05 differs from HEAD blob 0acfa6bfe, anchored old-spelling count 1 and new-spelling count 0. Restore PROVEN after: blob back to 0acfa6bfe, equal to the HEAD blob, and git diff HEAD empty. Restore leg rebuilt too, and dist confirmed clean of the mutation (dist/audit/index.d.ts hash back to aaf79bea0; built index.mjs carries the to_addresses spelling once and the bare 'to' spelling zero times), so no later measurement ran on a mutated tree. Absolute paths and an EXIT/INT/TERM trap throughout. RESULT: dist/index.d.ts IDENTICAL between legs (358c96d34 both) -- and that is the assumption-2.5 falsification, because it is an 11-line re-export barrel that cannot change for a data edit. dist/audit/index.d.ts DIFFERS by exactly one line: readonly highlightFields: [\"subject\", \"to\", ...] becomes [\"subject\", \"to_addresses\", ...], since ObjectSchema.create preserves literal types. The merge does not disturb this: it touched no file under packages/spec/src/data except document.zod.ts, and object.zod.ts is untouched. OTHER MEASURED FACTS: the 422 path confirmed by reading runtime-authoring-gate.ts (code INVALID_METADATA, status 422); the boot-unaffected claim confirmed -- EmailServicePlugin registers SysEmail through the manifest service, a path with no authoring gate; no generated artifact carries highlightFields (0 hits across every *.generated.ts), and the i18n source-hash ledger keys on translatable leaves only, so no regeneration is owed. NOT DONE, DECLARED: the repo-wide pnpm lint sweep (CI owns it) and the full workspace build the 3 exit-3 gates need.", "mcp_calls": "2 -- one search_issues (failed: 'API rate limit already exceeded', GraphQL pool empty) and one create_pull_request. Everything else went through the zero-quota channels: git, and repo-scoped REST via container curl (probed green at the start of the round). REST /search/issues is NOT available to this session -- it answers 403 'sessions are bound to their configured repositories', with a repo-scoped control returning 200 in the same read -- so the mandatory dedupe search had no working channel; see out_of_scope_findings.", "open_questions": [ { "question": "Clause-2 limb 1: does a changed string LITERAL inside an emitted .d.ts type for a shipped data declaration count as 'widening the published surface'? I declared 'no' and shipped, but the line is PM's to draw and the measurement makes it a real question rather than a theoretical one.", "options": [ "A. No (what I declared). No exported symbol is added, removed or renamed and no signature shape moves; only a literal value inside an inferred data type changes. Reading it as 'yes' would make EVERY content edit to any shipped *.object.ts Clause-2 -- a label, a description, a picklist option -- because ObjectSchema.create preserves literal types for all of them.", "B. Yes, on the literal instrument the dispatch word states ('does any exported symbol/signature move'), since the emitted type for SysEmail is demonstrably not byte-identical. This routes ordinary platform-object content maintenance to the contract-review tier.", "C. Narrow the instrument itself: judge limb 1 on the .d.ts diff EXCLUDING literal-value changes inside object declarations, and say so where the instrument is written, so the next seat does not have to re-derive this." ], "recommendation": "A, and worth pairing with C. The measurement, not the theory, is the argument: the emitted type changes for every data edit in this package, so B has no stopping rule and would re-tier routine maintenance. Separately, the dispatch word's named instrument (dist/index.d.ts) is BLIND here -- it is a re-export barrel -- so whichever way this is decided, the instruction should name the SUBPATH entry points rather than the root barrel, or the next seat measures the wrong file and reports a confident false negative." } ], "out_of_scope_findings": [ "NOT FILED -- no dedupe channel, handing to PM: packages/platform-objects/vitest.config.ts documents its @objectstack/lint alias as existing for 'managed-api-method-affordance-sweep.test.ts (#7934) -- the only suite here that imports a sibling package as a VALUE'. That is already false on main before my change: sys-user-preference.organization-unique.test.ts and pages/canonical-expression-envelopes.test.ts both import values from @objectstack/lint, and my new test makes four. Observation class, label 'finding'. It matters slightly more than a stale comment usually would, because the comment is the recorded rationale for a load-bearing alias: a future author trimming on 'the only suite' could delete the alias believing one test's removal freed it. I did NOT file it: the mandatory dedupe search had no channel (MCP search_issues returned 'API rate limit already exceeded'; REST /search/issues answers 403 for this session), and filing without dedupe is the forbidden shape. I left vitest.config.ts untouched.", "RETIRED ON INSPECTION, recorded so PM does not re-open it: I nearly filed 'packages/platform-objects excludes **/*.test.ts from its typecheck program, hiding 35 test files and 3 real type errors'. It is NOT a finding -- it is a ledgered, shrink-only TEST_DEBT entry in scripts/check-type-check-coverage.mjs with the count measured exact, and check:type-check-coverage is green on it. Checking before filing is what stopped it." ] }
Generated by Claude Code
packages/platform-objects/src/audit/sys-email.object.ts:32declares:The object has no field named
to. Its recipient column isto_addresses(sys-email.object.ts:59); the other three entries (subject,status,sent_at) all resolve. SohighlightFields[1]is a dangling field reference on a shipped platform object.How it was found
Measuring the shipped object corpus for #15495 — every
*.object.tsunderpackages/andexamples/, 116 objects, each pushed through the real object write door (runRuntimeAuthoringRules({ type: 'object' })) with its siblings as context. Two objects raise anerrorthere, and this is one of them:The finding is
object-field-ref-unknown, the rule #15254 added and crossed onto the object write door. It is not raised by anything this measurement added — it is the state ofmaintoday.Why it matters
highlightFieldsdrives the object's default list columns, record cards, previews and the detail highlight strip. Every consumer silently skips an entry it cannot resolve, so each of those surfaces renders one field short with no error anywhere — the recipient column, on the platform's own email log.There is a second consequence now that the object door gates this rule:
sys_emailin its current shape cannot be republished throughPUT /api/v1/meta/objector a package publish — the door answers 422INVALID_METADATA. It ships as a registry object today, so nothing is broken at boot; it is a live trap for anyone who edits it through a door rather than the file.Fix
Rewrite the entry to the real column name:
Deliberately left for its own card rather than ridden along on #15495: that PR is scoped to
packages/lint, and this is a one-line content fix inpackages/platform-objectswith its own package and changeset.Also seen in the same sweep (separate, lower confidence)
dc_account(packages/qa/downstream-contract/src/account.object.ts) declares nosharingModel, so the same door raisessecurity-owd-unsetaterror. That may well be intentional for a minimal contract fixture — noted here rather than filed, so the sweep's second finding is not lost.Generated by Claude Code