Skip to content

feat(metadata): give four declared-but-unnamed fields the consumer they should already have - #1831

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1826-field-consumers-six
Sep 9, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-1826-field-consumers-six

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Part of #1826

⚠️ Deliberately not Fixes — 4 of the 6 fields are cleared, and 2 are reported standing because each needs a product decision this PR must not guess at. #1826 stays open for those two. See The two that stand below.

Description

field-no-consumers is a new rule in @objectstack/lint@17.4.0. It fires on a declared field that no metadata names — "no view column, form section, page binding, flow node, dataset, widget, formula, validation, hook or action names it". Every field in this card was already stored and already editable, because these objects render through the synthesized detail/form layout and a synthesized layout names nothing. So the gap was a display gap, not a dead field, and the remedy is to give each field the consumer it should already have.

⛔ Nothing here is suppressed, whitelisted, locally re-severitied or deleted.

⭐ The empirical check the card gated everything on — it HELD

The card's own words: the claim that adding a view column clears field-no-consumers is the PM's reading of the rule's wording, to be verified on the first field before grinding through the rest.

Measured on this branch's base, 965933b:

stage errors warnings suggestions
base 965933b 0 13 12
after fixing one field (crm_campaign.description, a form-section binding) 0 12 12
final, this PR 0 9 12

The single-field probe dropped the count by exactly one and objects[2].fields.description left the issue list. The remedy is correct for this rule. Parsed with raw_decode from the first {, as the card prescribes.

Warning breakdown, final: field-no-consumers 12 → 8, component-props-unknown-key 1 → 1 (#1216's, ⛔ untouched). Of the 8 that stand, 5 are the crm_contact.mailing_* group and 1 is crm_forecast.seed_key — both explicitly out of this card — and 2 are reported below.

Changes Made

1. crm_account.logo — cleared

Two bindings, because one without the other would be half a feature.

  • gallery.coverField on the account_gallery view. The view is literally labelled Branded account cards and had a brand colour and no brand image; logo is the object's only image field. logo also joins that view's columns, for the reason the campaign_timeline view already records: the list data source projects the configured columns, and the cover binding reads off that projection. coverFit is left unstated — the schema default is the crop these cards want, and materialising a default rewrites "the author said nothing" into "the author asked for this".
  • A form field in the profile section, beside brand_color, full span. Without it there is no upload control anywhere in the app, so the cover could never have been filled.

2. crm_campaign.description — cleared

Added to the basic form section on campaign.view.ts. That section already enumerates the basic group field by field and had simply omitted the markdown brief — which seven seed campaigns fill and no screen would show, and which no marketer could write.

3. crm_contract.description — cleared, and the card's question answered

Added to the notes form section, above special_terms, in a one-column section suited to long-form text.

The card asked which of the two markdown bodies a reader is meant to use, and this is answerable from the repo rather than by preference: content/docs/revenue/contracts.mdx sends every free-text term to Special Terms by name three separate times — renewal instructions, a billing cadence the picklist will not hold, a payment arrangement the picklist will not hold — while listing Description in the Contract Information group as part of the record's identity block. They are not redundant. Description was the one field in that documented inventory no form offered, so the summary the guide tells a reader the record carries could not be written. The rationale is a comment beside the code.

4. crm_campaign_member.added_date — cleared

relatedListColumns on the crm_campaign lookup: ['crm_lead', 'crm_contact', 'added_date', 'status', 'response_date'].

⭐ This is the one mechanism that actually reaches that panel, and it is measured in this repo rather than assumed — test/view-references.test.ts (#944) records that a detail-page related list reads relatedListColumns on the child's lookup first and only then falls back to the child's highlightFields minus that lookup; the child's list view is never consulted. So authoring a crm_campaign_member view bundle would have satisfied the linter and shown a user nothing.

The value is a superset of the fallback it replaces (Lead / Contact / Status / Response Date) plus the missing column, ordered the way the row happens: who was enrolled, when, then how they answered.

⛔ Deliberately not solved with highlightFields. added_date is the only field in the basic group that is neither the record title nor already in the highlight strip. Hoisting it would leave the group with nothing of its own and silently vanish its heading from every detail page (field-group-shadowed) — the exact trap that object's own fieldGroups note records being rescued from (#715, #582).

This also settles the tension the card flagged: #1807 / PR #1814 built two elevated sub-flows so this stamp keeps being written, and the rule was reporting that nobody reads it. The stamp is now legible where a marketer reads memberships. ⛔ Nothing about the field's declaration, its readonly: true (#1667) or its writers changed.

5. Documentation

Two user-visible panels changed shape, so their pages follow — all three locales each:

  • content/docs/sales/accounts.mdx (+ .zh-Hans / .zh-Hant) — the Branding section row named only the brand colour, and the Account Cards row described a card with no image.
  • content/docs/marketing/campaign-members.mdx (+ .zh-Hans / .zh-Hant) — the page stated the panel's columns as four, named them, and explained that a related list's columns "come from the object's highlight fields". The first is now five, and the second is superseded by the mechanism this PR uses. The load-bearing half of that sentence — a related-list panel never reads the child's list view — is kept, because it is still true and is what stops the next author authoring one.

⛔ content/docs/releases/ untouched. The changeset is this PR's input to the release notes.

⚠️ The two that stand — each needs a decision, and neither is guessed at here

crm_quote_line_item.line_number — standing

The card's candidate was a related-list column or sort key. That candidate is wrong, and the repo says why: the field has no writer.

readonly: true, and grep finds no hook, no flow node and no action that writes it. quote_generation creates the quote and no line items at all, so quote lines are only ever created by hand — and since objectql 17.4.0 a static readonly field is stripped from a non-system caller's INSERT too, so a rep cannot seed it either. The only values in existence are the 19 seed rows; test/seed-consistency.test.ts pins those to 1..n per parent, which is a pin on the fixture, not evidence of a runtime writer.

⇒ A column or sort key here would render blank on every line a user creates and show a number only on demo data. That is a fabricated surface, which the card ranks below an honest standing warning.

The decision needed: give the ordinal a writer (a per-parent assigner on insert, in the shape _line-item-price-fill.ts establishes — and because the field is readonly that writer must be system-context, which puts it under AGENTS.md house rule 9, the same question #1807 settled for added_date), or retire the ordinal and its 23 carrier sites. Filed as #1828, which also records that the twin crm_opportunity_line_item.line_number has the same gap and is shipped in the billing hand-off payload on every won deal — so whichever answer is chosen should cover both objects.

crm_article_feedback.comment — standing

The card anticipated this one may come back as "retire the column". It comes back as a genuine product decision, and here is the measurement behind it.

The field has neither a writer nor a reader. The only things that create feedback rows are mark_article_helpful / mark_article_not_helpful — one-click header buttons whose bodies write crm_knowledge_article, verdict and owner_id and nothing else. A reader is never asked for a note. And its description promises "Optional note explaining the verdict — read by the article's author", while no surface offers the author anything of the kind; the object's own docblock places the comment as "the group's own content, one click into the row" on the feedback record's synthesized page, which is not a thing an author is ever routed to.

The decision needed, and it is not settleable from this repo:

  1. Build what the description promises — a capture step (which cannot be a type: 'script' action: those bodies collect no input, so the one-click thumb would have to become a screen flow) and an author-facing surface. That is a feature with a real UX cost: asking for a note suppresses votes, and this object exists to make two counters trustworthy.
  2. Retire the column and its 4 carrier sites, and delete the promise in the description with it.

⛔ Not retired here, per the card's explicit instruction.

Testing

  • pnpm verify green end to end — validate · typecheck · lint · lint:i18n-gate · hygiene · hygiene:tokens · build · test
  • pnpm test — 165 test files passed, 3454 passed, 1 skipped, 0 failed (unchanged from base; ⛔ no test added — AGENTS.md scope rule 3, this repo does not grow a gate farm for a platform rule)
  • pnpm lint --json — errors: 0 · warnings: 9 · suggestions: 12, passed: true
  • pnpm lint:i18n-gate — 0 i18n/missing-* issues. No new locale key was needed: all four fields already carry labels in en / zh-CN / es-ES / ja-JP, every form section reused is an existing _sections key, and relatedListColumns adds no translatable surface.
  • pnpm hygiene:tokens — authored total ~137,818 (ceiling ~140,000); business semantics ~85,291; interaction layer ~37,882. All clean, no ceiling moved.
  • Changeset added (.changeset/four-fields-get-the-surface-they-promised.md), written for the release-notes reader.

Additional Notes

Out-of-scope findings filed rather than fixed here: #1828 (the line_number writer gap, both line-item objects, plus the billing payload that ships the always-null field) and #1829 (campaign-members docs still describe the tracker fields and statuses #597 removed, three locales).

⛔ Untouched, as the card directs: the five crm_contact.mailing_* fields, crm_forecast.seed_key (correct by design, upstream as objectstack#17135), the component-props-unknown-key warning (#1216's), and the --strict flip itself (#1581).

⚠️ This PR stays a draft. 「等总监复审」 — ⛔ not marked ready, ⛔ no auto-merge, whatever CI reports.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj


Generated by Claude Code

…ey should have

`field-no-consumers` (new in `@objectstack/lint@17.4.0`) fires on a declared
field no *metadata* names. All four below were already stored and editable
through the synthesized layout — a synthesized layout names nothing — so the
gap was a display gap, not a dead field. Nothing is suppressed, whitelisted,
re-severitied or deleted.

- crm_account.logo — `coverField` on the `account_gallery` view ("Branded
  account cards" had a brand colour and no brand image), plus a form field in
  the `profile` section beside `brand_color`, without which there was no
  upload control anywhere in the app.
- crm_campaign.description — the `basic` form section, which already
  enumerates that group and had omitted the markdown brief seven seed
  campaigns fill.
- crm_contract.description — the `notes` form section, above `special_terms`.
  Which of the two markdown bodies a reader uses is settled by
  `content/docs/revenue/contracts.mdx`, which sends every free-text TERM to
  Special Terms by name three times.
- crm_campaign_member.added_date — `relatedListColumns` on the `crm_campaign`
  lookup. That is the one mechanism a detail-page related list actually reads
  (#944); a `list` view is never consulted, and adding the field to
  `highlightFields` would hoist the `basic` group's only remaining field and
  shadow the group.

Docs follow the two panels that changed shape, in all three locales.

`crm_quote_line_item.line_number` and `crm_article_feedback.comment` are left
standing on purpose — each needs a product decision, stated in the PR body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 9, 2026 2:24pm UTC

Request Review

@github-actions github-actions Bot added the metadata Declarative metadata — schema, security posture, UI surfaces label Sep 9, 2026
@os-steve
os-steve marked this pull request as ready for review September 9, 2026 23:41
@os-steve
os-steve added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 89874d2 Sep 9, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants