Skip to content

sys_email.highlightFields names to, but the field is to_addresses — a dangling reference on a shipped platform object #15629

Description

@claude

packages/platform-objects/src/audit/sys-email.object.ts:32 declares:

highlightFields: ['subject', 'to', 'status', 'sent_at'],

The object has no field named to. Its recipient column is to_addresses (sys-email.object.ts:59); the other three entries (subject, status, sent_at) all resolve. So highlightFields[1] is a dangling field reference on a shipped platform object.

How it was found

Measuring the shipped object corpus for #15495 — every *.object.ts under packages/ and examples/, 116 objects, each pushed through the real object write door (runRuntimeAuthoringRules({ type: 'object' })) with its siblings as context. Two objects raise an error there, and this is one of them:

ERR sys_email: object-field-ref-unknown objects.sys_email.highlightFields[1]
  highlightFields[1] "to" is not a field on object "sys_email".

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 of main today.

Why it matters

highlightFields drives 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_email in its current shape cannot be republished through PUT /api/v1/meta/object or a package publish — the door answers 422 INVALID_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:

highlightFields: ['subject', 'to_addresses', 'status', 'sent_at'],

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 in packages/platform-objects with 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 no sharingModel, so the same door raises security-owd-unset at error. 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

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · 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:

    1. 界面少一列,且无声。 highlightFields 驱动默认列表列、记录卡、预览与详情高亮条;每个消费者对解析不了的条目静默跳过 ⇒ 平台自己的邮件日志上,收件人那一列在这些面上不出现,而没有任何地方报错。
    2. 该对象现在过不了自己的写门。 自 [P0] Studio package publish accepts a dangling highlightFields reference — 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 重新发布(422 INVALID_METADATA)。

    不给 p1 的理由:它以 registry 对象形态出厂,启动不受影响,今天没有任何东西是坏的运行时。这是一个「谁通过门去编辑它谁就撞墙」的活陷阱,不是正在造成损害的故障。

    不给 p3 的理由:修复是一行,而后果是平台自有对象上的一处可见缺列 + 一道自我封锁的发布门。让它留着的成本远高于修它。

    交给接手者的三条

    1. 只改那一行。 ['subject', 'to_addresses', 'status', 'sent_at']。⛔ 不要顺手重排 highlightFields、不要顺手加列。
    2. 补一条防复发的判据,但别越界。 真正耐久的做法是让 object-field-ref-unknown 对出厂对象语料成为红门(今天它只在写门上);⚠️ 那是一次门禁扩面,不在本卡的一行范围内 ⇒ 若判断值得,另开卡,⛔ 不要挂在本 PR 上。本 PR 内的合理增量最多是一条针对 sys_email 的断言。
    3. ⛔ 不要顺手处理 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

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claimed by the PM dispatch loop.

    Claim: session session_01ARYe3yQTQCUFm5qPYNgKaJ, branch claude/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.ts declares no sharingModel, so the same door raises security-owd-unset at error — 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-unknown into 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 about sys_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:

    1. A silently missing column. highlightFields drives 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.
    2. The object cannot be republished through its own write door. Since [P0] Studio package publish accepts a dangling highlightFields reference — the authoring gate never sees what the app builder produces #15254 crossed object-field-ref-unknown onto the object write door, sys_email in this shape gets 422 INVALID_METADATA from PUT /api/v1/meta/object or 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 got ERR 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_email currently 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 ruled patch/no on earlier today. Re-derive it on @objectstack/platform-objects and state both limbs separately.

    ⚠️ This Clause-② 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 declared no for 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 for packages/spec. For any other package the published surface is its files[] + types (dist/**). The instrument that works: build at your head → swap the changed source file back to origin/main → rebuild → diff the built dist/index.d.ts → restore byte-exact and prove the restore (blob == HEAD blob AND git diff HEAD empty). 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-test reddened origin/main's own push build and six PRs today; PR #16002 landed as f50c394da (verified by content on origin/main). If you cut from current main you will not see it. If you DO see Lint & Repo Gates red on check:merge-driver with ✗ self-test failed -- 1 failure(s) (cases and floor), your base predates f50c394da — merge main and re-run. ⛔ Do not skip, disable or quarantine anything over it.

    ⭐ Discipline notes earned the hard way today, all from real failures:

    1. An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own EXIT trap 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, or git hash-object != HEAD blob) and prove the restore after, in one shell.
    2. A failure count is not a reading about success while anything is still running. Read status explicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed.
    3. 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.
    4. cancelled is not failed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run is lint.yml's concurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run's created_at.
    5. The shared verify lock is contended. scripts/pm/os-verify-lock.sh returning 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.
    6. Shared-ref hazard: origin/main moves 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 built dist/**/*.d.ts, dist/**/*.{js,mjs,cjs} and content/docs/**, not a guess.

    ⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never git stash — refs/stash lives in the common .git dir, so all worktrees share one LIFO stack and your pop takes 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.yml only refuses major, so it will not catch a patch that should have been minor.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions