Skip to content

plugin-audit's recordLabel guesses from a fixed key list instead of the ADR-0079 resolver, so an object titled by company_name shows its record id on every activity row #21878

Closed

Description

@hotlong

Source

objectstack-ai/cloud acceptance run on 2026-10-05 (cloud main 4b38cd28 · objectstack main 088428fb · objectui main 76993f8e). A new free user had the AI build a 客户 object. Its fields are company_name / phone / status; it has no name field and declares no nameField. Every row on the record's discussion feed reads:

Created 客户 "RZGAEnis7ms__pjv"
Updated 客户 "RZGAEnis7ms__pjv"

sys_activity.record_label holds the record id for all 15 rows of that object. The record page header shows 「宁波海晟进出口有限公司」 for the same record, because the renderer resolves the title through ADR-0079.

(The English verb is a separate cause: the workspace has no default locale. That half is tracked on the cloud side, not here.)

Mechanism

packages/plugins/plugin-audit/src/audit-writers.ts:299 recordLabel() guesses the label from a fixed list of keys and falls back to the id:

const candidates = ['name', 'subject', 'title', 'full_name', 'label', 'first_name', 'company', 'email'];

company_name is not in that list. The same file already imports resolveDisplayField (line 28) and uses it for referenced-record titles (~line 1303). So the activity label is a second display-name dialect beside the ADR-0079 resolver that every renderer uses.

Measured against the spec on objectstack main 088428fb:

  • resolveDisplayField({ fields: { company_name, phone, status } }) → company_name
  • resolveDisplayField({ fields: { visit_title, visit_date, visit_summary, customer } }) → visit_title

Fix direction

Resolve the label through resolveDisplayField(objectDef), and keep the id only as the floor when that resolves nothing or the value is empty. Keep the field answer that #21081's read-side gating relies on: it becomes the resolved field.

Acceptance

  1. A record of an object titled only by company_name gets record_label = the company name, and field = company_name.
  2. Objects that have name / title / subject are labelled exactly as today.
  3. An object whose resolved title value is empty still falls back to the id.
  4. security(plugin-audit): an activity row composed at write time may carry a changed field's values to a reader who can read the parent record but not that field (unmeasured; measure first) #21081's masking still holds: a reader who is not served the resolved field does not see the label.

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Path: business objects and records — record changes are visible | records-forms.field-history-tracking | P2

    Triage: first grade — bug · priority:p2 · domain:services · area:records (the filer's bug stands) · pm:queue. The activity label resolves through ADR-0079's resolver; the id stays only as the floor

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T13:52Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/plugins/plugin-audit/src/audit-writers.ts (recordLabel, about :299) ⇒ domain:services; rationale: a fixed key list is a second display-name dialect beside resolveDisplayField, which the same file already imports.


    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 5, 2026
  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 3 · 2026-10-05T15:14Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21878-activity-label-display-field
    Worktree: objectstack-issue-21878
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main 07c842df), per triage's direction 5995881267:

    ⛔ The fixed key list is deleted, not extended. ⛔ No change to #21081's read-side masking: a reader not served the resolved field does not see the label. ⛔ No packages/spec edit.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier).
    Clause-②: no


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 21878,
    "status": "done",
    "branch": "claude/issue-21878-activity-label-display-field",
    "pr": "#21896",
    "session": "session_011K3zqE8Pv1Evw5hc8tZCnN — the dispatching PM session (this run is its subagent)",
    "premise_still_valid": true,
    "summary": "plugin-audit's activity mirror now labels a record by its ADR-0079 title: a new titleFieldOf(objectName) closure in audit-writers.ts asks resolveDisplayField over the cached getObjectDef definition (no second lookup) and floors to the id when nothing resolves, the resolver lands on id, or the title field is a credential or internal; resolveLookupTitles now asks the same closure (its three inline skips moved there, behaviour-identical). recordLabel(record, titleField, id) reads that field from the same masked ledger view, floors a null, structured or blank value to the id (never borrowing another field), and returns the resolved field, which the row declares as the label's source, so #21081's read-side redaction is unchanged and now keys on company_name. The fixed key list is deleted. Measured H3 (registered through a real ObjectQL registry): of 32 example objects 27 keep their label field and 5 change (crm_contact full_name to first_name because its full_name formula declares no returnType text; crm_opportunity_line_item, showcase_expense_line, showcase_ext_order, showcase_invoice_line move from the id to a field); of 50 platform objects 24 move from the id to their nameField and 3 change field (sys_email_template, sys_invitation, sys_scim_connection_credential). Stored rows are not backfilled.",
    "tests": "All at head e0da4f8 unless stated. (1) plugin-audit: vitest run --maxWorkers=2, 40 files / 641 tests passed. (2) plugin-audit typecheck exit 0, check:test-typecheck OK 0 debt (run at 1088327; git diff 1088327 e0da4f8 -- packages/plugins/plugin-audit is empty). (3) dogfood --project isolated: new activity-label-display-field.dogfood.test.ts + sibling activity-field-values.dogfood.test.ts, 2 files / 14 tests passed against a rebuilt plugin-audit dist (titleFieldOf present 3x in dist/index.mjs, ablation marker 0x); dogfood typecheck exit 0. (4) New unit file audit-record-label-display-field.test.ts: 11 tests passed (company_name on create/update/delete with declared source, name/title/subject unchanged, explicit nameField over name, blank-title and empty-name-beside-title floors to id, credential title floors to id, unserved reader gets neither label nor summary, control keeps both). (5) Fixture triage audit-writers.test.ts: 4 localized-summary cases wrote records of person_qualification, an object the fake engine never declared, and passed only via the key list; disposition: declare the object titled by name; 93/93 pass. (6) Narrowed lint: eslint --no-inline-config --format json over the 4 changed TS files: 4 files, 0 errors, 0 warnings; population: each file resolves under --print-config; invariance: eslint.config.mjs enables no type-aware linting, so this diff cannot move a verdict on an untouched file; pnpm lint is CI's. ABLATION (fixed key list put back, scripts/ablation-replace.mjs, anchor hit 1, blob 47497d1d8eed to 61d82281885f, fix committed first, trap restore armed): unit leg (source-resolved) 7 red / 4 green exactly as predicted, first red 'expected 3JO4ITH0my2YkCtR to be ARDCOMPANYONE41' (the card's symptom), restore blob 47497d1d8eed == HEAD and git diff HEAD empty; dogfood leg (dist-resolved) mutate + rebuild (build exit 1 with output discarded; ablation-dist-preflight found the marker in both JS bundles, exit 0) then 2 red (company_name label case; withheld reader served a record_label) / 2 green (name case, empty-title floor), restore blob == HEAD, rebuilt, preflight --absent exit 0 (marker absent from all 6 built files, tree clean), then the green dogfood run in (3).",
    "mcp_calls": "0 — none",
    "api_writes": "3 — all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft, #21896, run 37340288214, body read back identical 8619 bytes); (2) label-write assignee = POST /repos//issues/21896/assignees os-steve (run 37340384784, output says MATCHES via the relay run; zero labels in target); (3) this os-dev-report comment = POST /repos//issues/21878/comments via scripts/pm/post-stamped.mjs. git push is not counted (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted, not filed · crm_contact in examples/app-crm titles by first_name because its full_name formula declares no returnType text, so ADR-0079 does not derive it; the activity label now agrees with the record page; whether the example should title contacts by full_name is the example's call (observation, no class a/b/c)",
    "carrier: none (承接者:无) · noted, not filed · audit-writers.ts getObjectDef/getFieldDefs caches are never invalidated, so a definition changed at runtime may be read stale by the label, milestones and enable.activities alike; read from source only, not measured, predates this change",
    "carrier: none (承接者:无) · noted, not filed · boundary: a formula title (display_title on several platform objects) is declared as the label's only source though computed from other fields; a reader served the formula is served the same value by the data plane, so the label adds no exposure"
    ],
    "gates": {
    "derived_by": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at e0da4f8 (merge base e085a8c, 5 paths)",
    "commands": [
    "exit 0 :: node scripts/check-adr-0087-registration.mjs --base origin/main",
    "exit 0 :: node scripts/check-adr-0087-registration.mjs --self-test",
    "exit 0 :: node scripts/check-changeset-no-major.mjs --base origin/main",
    "exit 0 :: node scripts/check-changeset-no-major.mjs --self-test",
    "exit 0 :: node scripts/check-ci-filter-parity.mjs",
    "exit 0 :: node scripts/check-closing-keyword-parity.mjs",
    "exit 0 :: node scripts/check-closing-keyword-parity.mjs --self-test",
    "exit 0 :: node scripts/check-comment-mask-adoption.mjs",
    "exit 0 :: node scripts/check-comment-mask-adoption.mjs --self-test",
    "exit 0 :: node scripts/check-comment-mask-corpus.mjs",
    "exit 0 :: node scripts/check-dts-emitted.mjs --self-test",
    "exit 0 :: node scripts/check-empty-changeset.mjs --base origin/main",
    "exit 0 :: node scripts/check-empty-changeset.mjs --self-test",
    "exit 0 :: node scripts/check-issue-citations.mjs",
    "exit 0 :: node scripts/check-keyed-text-bounds.mjs",
    "exit 0 :: node scripts/check-keyed-text-bounds.mjs --self-test",
    "exit 0 :: node scripts/check-platform-object-tenancy-census.mjs",
    "exit 0 :: node scripts/check-platform-object-tenancy-census.mjs --self-test",
    "exit 0 :: node scripts/check-plugin-teardown-shape.mjs",
    "exit 0 :: node scripts/check-plugin-teardown-shape.mjs --self-test",
    "exit 0 :: node scripts/check-registry-log-declared.mjs",
    "exit 0 :: node scripts/check-registry-log-declared.mjs --self-test",
    "exit 0 :: node scripts/check-rest-log-spy-declared.mjs",
    "exit 0 :: node scripts/check-rest-log-spy-declared.mjs --self-test",
    "exit 0 :: node scripts/check-system-context-census.mjs",
    "exit 0 :: node scripts/check-system-context-census.mjs --self-test",
    "exit 0 :: node scripts/check-tenant-audit-census.mjs",
    "exit 0 :: node scripts/check-tenant-audit-census.mjs --self-test",
    "exit 0 :: node scripts/check-undeclared-dep-imports.mjs",
    "exit 0 :: node scripts/check-undeclared-dep-imports.mjs --self-test",
    "exit 0 :: node scripts/docs-audit/check-affected-docs.mjs",
    "exit 0 :: node scripts/docs-audit/check-drift-comment.mjs",
    "exit 0 :: node scripts/pm/release-rehearsal-clone.mjs --self-test",
    "exit 0 :: node scripts/release-pending-publish.mjs --self-test",
    "exit 0 :: pnpm --filter @objectstack/spec run check:duration-unit-keys",
    "exit 0 :: pnpm --filter @objectstack/spec run check:empty-state",
    "exit 0 :: pnpm --filter @objectstack/spec run check:liveness",
    "exit 0 :: pnpm --filter @objectstack/spec run check:strictness-ledger",
    "exit 0 :: pnpm --filter @objectstack/spec run check:variant-docs",
    "exit 0 :: pnpm check:changeset-gate-self-tests",
    "exit 0 :: pnpm check:cross-package-test-inputs",
    "exit 0 :: pnpm check:dispatcher-error-vocabulary",
    "exit 0 :: pnpm check:doc-authoring",
    "exit 0 :: pnpm check:driver-memory-census",
    "exit 0 :: pnpm check:dts-closure (first reading exit 1: hono declarations missing while this worktree's own closure build rewrote that dist; re-run exit 0, 169/169 present)",
    "exit 0 :: pnpm check:dual-build-cjs-loads (first reading exit 3 PREREQUISITE NOT MET: 9 packages unbuilt in this worktree; re-run after full turbo build exit 0)",
    "exit 0 :: pnpm check:engine-double-contract",
    "exit 0 :: pnpm check:gitlink-declared",
    "exit 0 :: pnpm check:i18n",
    "exit 0 :: pnpm check:i18n-stale-fill",
    "exit 0 :: pnpm check:issue-citations",
    "exit 0 :: pnpm check:lean-entry-closure",
    "exit 0 :: pnpm check:logger-receiver-detach",
    "exit 0 :: pnpm check:nul-bytes",
    "exit 0 :: pnpm check:objectql-double-limit",
    "exit 0 :: pnpm check:objectui-changeset",
    "exit 0 :: pnpm check:org-identifier",
    "exit 0 :: pnpm check:page-declaration-shape",
    "exit 0 :: pnpm check:pm-changeset-deadline-census",
    "exit 0 :: pnpm check:published-files",
    "exit 0 :: pnpm check:query-options-erasure",
    "exit 0 :: pnpm check:refd-timer-probe",
    "exit 0 :: pnpm check:slot-lookup",
    "exit 0 :: pnpm check:sourcemap-no-sources-content",
    "exit 0 :: pnpm check:test-source-alias",
    "exit 0 :: pnpm check:tier-file-adoption",
    "exit 0 :: pnpm check:type-check-coverage",
    "exit 0 :: pnpm check:type-check-debt",
    "exit 0 :: pnpm check:watch-hint-literal",
    "exit 0 :: pnpm check:where-matcher"
    ],
    "ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack: exit 0, 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (derived zero, all 70 recorded an exit code)",
    "dispatch_list_compare": "the dispatch-time list (62) is a strict subset of the derived 70; the 8 extra are the changeset-kind families (adr-0087-registration x2, empty-changeset x2, release-rehearsal-clone, release-pending-publish, objectui-changeset, pm-changeset-deadline-census)"
    },
    "line_budget": "n/a",
    "deviations": [
    "Worktree based on origin/main e085a8c (the dispatch base 07c842d had moved); origin/main later advanced 4 commits with zero overlap with this diff or the gate derivation, not merged; the queue rebuilds on current main.",
    "H1 reproduce-first: the red reading on a company_name-titled object was taken by ablation (the fixed key list put back over the committed fix, same selection code as main) rather than before writing the fix.",
    "resolveLookupTitles was touched: its id/credential/internal skips moved into the shared titleFieldOf so the record label and referenced titles share one answer; behaviour-identical, its existing pins (audit-lookup-summary, milestone, implicit-reference-target) pass.",
    "An existing plugin-audit test file was edited (audit-writers.test.ts, fixture triage: declare the undeclared fixture object); within the declared plugin-audit test surface; no existing packages/qa file was edited.",
    "The credential-title unit case uses a password field: a secret field needs a crypto provider and the sys_secret table, which this harness does not carry; both are masked by the same collectMaskedReadFields predicate.",
    "The dogfood door pin also carries the masking case (a permission set withholding company_name; control served it), beyond the three named cases.",
    "Two gates read red first on build state, not on this diff (check:dts-closure raced this worktree's own closure build; check:dual-build-cjs-loads PREREQUISITE NOT MET); both re-run 0 and the --ran record carries the re-run codes.",
    "Labels documentation, size/l, tests, tooling on #21896 were applied by the platform labelers, not by this run."
    ],
    "files_changed": [
    ".changeset/21878-activity-label-display-field.md",
    "packages/plugins/plugin-audit/src/audit-writers.ts",
    "packages/plugins/plugin-audit/src/audit-writers.test.ts",
    "packages/plugins/plugin-audit/src/audit-record-label-display-field.test.ts",
    "packages/qa/dogfood/test/activity-label-display-field.dogfood.test.ts"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Review: PR #21896 at e0da4f8a · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T16:27Z

    Verdict on the dev report 5998605968 (status: done), checked against GitHub and the tree. Accepted as built. No contract review is owed: Clause-②: no, and no packages/spec path is touched.

    Checked:

    • The fixed key list is deleted. A shared titleFieldOf(objectName) in audit-writers.ts runs resolveDisplayField over the cached definition (no second lookup). It skips a resolved id, a credential field and an internal field, the three exclusions referenced-record titles already applied. resolveLookupTitles now calls the same function, so the record label and referenced titles give one answer.
    • recordLabel(record, titleField, id) reads only the resolved field from the same masked ledger view. A null, structured or blank value falls back to the id and never borrows another field. It returns the resolved field as the label's source, so security(plugin-audit): an activity row composed at write time may carry a changed field's values to a reader who can read the parent record but not that field (unmeasured; measure first) #21081's read-side redaction applies unchanged, now to company_name too. A unit case and the door pin both prove that a reader not served company_name gets neither label nor summary, while the control reader keeps both.
    • H3, measured through a real registry: of 32 example objects, 27 keep their label field and 5 change. crm_contact moves from full_name, a formula with no text returnType, to first_name, its ADR-0079 title. Four move from the id to a field. Of 50 platform objects, 24 move from the id to their nameField, and 3 change field. All follow the one resolver, which is the card's point. The changeset names the CRM contact case.
    • Ablation (the key list put back): 7 unit pins red, the first one reading the card's own symptom, and 2 door legs red against a preflighted dist. The restore was proven.
    • Gates: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN.
    • One existing plugin-audit test fixture declared an object the fake engine never had. The fixture now declares it, inside the claim's test surface.

    Carried, not filed (in the PR's Acceptance notes): whether the CRM example should title contacts by full_name (the example's call); the audit-writers definition caches are never invalidated (read from source, not measured, and it predates this change).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    Contributor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T18:11Z


    Generated by Claude Code

  7. added a commit that references this issue on Oct 7, 2026
    b238856
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

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions