Skip to content

refactor(crm): enforce-or-remove every decorative field, one verdict each - #1195

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1182-decorative-field-sweep
Aug 17, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-1182-decorative-field-sweep

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #1182

The authorization

Maintainer ruling, 2026-08-17, quoted verbatim:

逐个 enforce-or-remove(推荐)

Chosen from the option described as: per-field adjudication in one card — give the three parents one rollup each or drop them, and delete the product inventory/tax surface, customer_signature, and the hour fields. Removing published fields is normally a maintainer-only decision; that ruling is that decision.

The unit of adjudication is the field. Ten rows, ten verdicts, each justified on its own below.

The measurement that decided three of the rows

Before judging the parent hierarchies I measured whether the "one rollup" the ruling offers is actually available on this platform, because the answer changes which verdict is honest. It is — and the cost argument I expected to make does not survive it.

Field.summary() is engine-computed (summaryOperations), and the engine handles it on a self-referencing relationship, where parent and child are the same object and the summary index is keyed by child. Measured on the installed 17.0.0 against the app's own booted stack, on all four legs of the lifecycle rather than at insert alone:

step expected measured
two children inserted (1,000 + 2,500) 3,500 3,500
one child raised to 4,000 6,500 6,500
other child re-parented away 4,000 4,000
remaining child deleted 0 0

So "enforce a parent hierarchy" costs one declaration — no hook, no recompute code, no backfill. That is not the same situation #1189 faced with next_renewal_date, where enforcement needed a cross-object min() over contract statuses and was rightly called feature design. With cost off the table, each parent row had to be decided on business need instead.

Row-by-row verdicts

# Field(s) Verdict Why
1 crm_product.quantity_on_hand, reorder_point remove Nothing ever decremented stock. The low_stock view they fed was not the report it looked like: its filter was quantity_on_hand <= 10, a hardcoded constant, not each product's own reorder point sitting in the next column. HotCRM sells from a catalog; a second copy of warehouse state is drift waiting to happen.
2 crm_product.is_taxable remove Confirmed the card's claim by reading the write path rather than trusting it: createLineItemPriceFill stamps list_price and nothing else, and crm_quote_line_item.tax_rate defaults to 0 and is typed per line. Clearing the flag on a zero-rated product changed no total anywhere.
3 crm_product.unit_of_measure, billing_type remove The one row where "seeded" made it look alive: 13 demo products carried both. No quote total, revenue report or line-item behaviour read either, while the docs claimed billing type "drives how the quote calculates totals". Enforcing would mean building subscription billing — a product, not a wiring job.
4 crm_case.customer_signature remove A signature pad on the resolution form that no close-case step, SLA measure or export ever read back. A signature nobody can retrieve is worse than no signature: it implies a record of acknowledgement that does not exist.
5 crm_task.estimated_hours, actual_hours remove No rollup summed them onto a case or opportunity, no variance report compared them, nothing warned when actual overran estimate. Effort on a task is progress_percent, which the views do read. A rollup could be declared, but nothing in this app asks what a case cost in hours.
6 crm_contact.reports_to remove The docs described "a clickable tree on the account detail page" that "the AI Copilot uses when summarising the account". Both were checked and neither exists: src/pages/account_detail.page.ts contains no contact component at all, and no skill reads the chain. Sharing derives from the master crm_account, never from this lookup.
7 crm_contact.birthdate remove The card allowed "a birthday automation would be the only honest keep". Building one is a new scheduled flow with month/day matching, not a declaration — and it is greeting-card scope for a B2B CRM. It is also the one row where inertness carries a second cost: an importable date of birth read by nothing is personal data held for no stated purpose.
8 crm_account.parent_account ENFORCE The only row with a published promise of the exact rollup: the accounts page sells the hierarchy with a diagram and states "roll-up reports — annual revenue of the global parent sums all children". Nothing computed it. child_account_revenue is now that sentence, in one declaration, measured above. Importability matters here and is why this row differs from #9 and #10 — see below.
9 crm_campaign.parent_campaign remove No page, doc, report or seed mentions campaign hierarchy anywhere; zero pull. And the suggested "hierarchy ROI rollup" is not one declaration: roi is a formula over each campaign's own actual_cost / actual_revenue, so a rolled-up ROI puts two differently-scoped ROI numbers on one record and leaves every reader guessing which they are looking at. Two truths on one object is the defect #1189 removed, not a fix.
10 crm_case.parent_case remove A count(child cases) rollup would be one declaration and would compute. It was still the wrong answer: nothing in the service model acts on case parenting — no mass close, no linked-case notification, no SLA inheritance — so the number would render on a form and change nothing. A computed number nobody acts on is the same decoration with arithmetic in it. Related cases that matter here already link through resolved_by_article, which the deflection measures do read.

What importability meant for #8, and why it did not save #7

parent_account is reachable from src/mappings/account_import.mapping.ts, so a customer's account file can populate the hierarchy today, and the import guide documents the column in three locales including the forward-reference behaviour. That is a write path with a real user behind it and no reader on the other side — data arriving into a column nothing consumes. Combined with the published rollup promise, that is the closest thing to measured pull any row in this card has, and it is what tipped #8 to enforce.

birthdate is importable in the same sense and still went. The difference is what the write was for: no doc promises a birthday capability, so the import column was collecting personal data toward a feature nobody had specified. Volume of a declared write path is not pull; a stated purpose is.

The removal takes the Birthdate column out of both halves of the contact template — the mapping and assets/import-templates/contacts.csv. The account template is untouched and still ships Parent Account.

Premise re-check (rule 6)

premise_still_valid: true for all ten rows — re-run per row on origin/main @ c83aa744, then widened, because the card flags its own scan as untrusted.

The card's scan is src/-scoped, *.ts-only and case-sensitive. Re-running it repo-wide over every file type found no consumer for any row, but it did surface three things the narrow form would have missed, and one of them was a real defect in this PR:

  • assets/import-templates/contacts.csv carries the Birthdate column, outside every src/-scoped scan. I removed the mapping and the guide, missed the CSV, and test/import-mappings.test.ts went red comparing the two. Fixed, not worked around, and test/decorative-field-sweep.test.ts now names that file so a future removal fails with a message that says which field regressed.
  • The zh docs found what the English grep missed. content/docs/guides/importing-your-data.mdx spells the column Birthdate (capitalised) and did not match a lowercase pattern; the zh-Hans/zh-Hant sweep by translated label (生日, 直属上级, 计费类型, …) surfaced four more affected pages — revenue/index, administration/setup, sales/activities, revenue/billing-handoff. The warned failure ran backwards this time: the localized pages caught the English page.
  • The scan under-reports by construction. crm_product.tax_rate is equally inert but never reached the card, because the token also matches lines in quote_line_item.object.ts — a different object's field. A false negative is invisible in a scan's output. Filed as crm_product.tax_rate is inert, and the consumer scan cannot see it — a same-named field on another object masks it #1193 with the methodology fix; not touched here, since it is not one of the ten rows.

Two view-side facts the views/-excluding scan could not show, both handled: low_stock is a whole view whose columns, filter and sort key are all removed fields, so it went with them; and no view hierarchy mode, referential action or controlled_by_parent derivation depends on any of the three self-lookups (crm_account and crm_case are private, crm_campaign is public_read, and crm_contact derives from its master-detail crm_account, not from reports_to).

What went with the removals

Field declaration, view columns / filters / sorts, form section entries, seed values, all four locale bundles, import mapping and CSV template, docs/developers/api_reference.md, docs/STATUS.md (345 → 334 fields), and every content/docs page that described a removed field in all three locales — 27 MDX files across revenue/products, revenue/index, revenue/billing-handoff, sales/contacts, sales/accounts, sales/activities, service/cases, administration/setup and guides/importing-your-data.

Three doc changes are judgement rather than deletion, so they are called out:

  • sales/accounts promised two things and delivered neither. The rollup is now real and documented precisely (direct children, one level, maintained on edit / re-parent / delete). The "cascading sharing" claim was simply false — crm_account is private — and is now stated as the non-behaviour it is, pointing at the sharing rules that do the job. [finding] The churn report never reads health_score — the one field a CSM hand-maintains for exactly this purpose #1186's health_score was left alone.
  • sales/contacts loses the org-chart section and gains a short note saying the tree never existed and naming what does work (title / department, which reports and the Copilot really read). A removal that leaves the reader wondering where their feature went is only half done.
  • service/cases carries exact field arithmetic ("16 of the object's 28", "the twelve that are not on the tab", "24 of the 28"). Recomputed against the object: 27 fields, 16 on the Details tab, 11 not, 7 of those on the form, 4 on no screen. This forced one correction beyond my rows: the page's field-group table claims to be "the one list that accounts for every field the object has" and was already missing resolved_by_article (28 stated vs 29 declared, predating this PR). Subtracting two would have made the page arithmetically false in a way a reader can check, so the missing entry is restored — the minimum edit that leaves the page true.

Gates run

All green at e16507d7, the final commit — the union was run after the last commit, at that sha.

Both gates the card named as friends did their job and neither was papered over: test/docs-drift.test.ts held docs/STATUS.md to the field count, and test/import-mappings.test.ts caught the CSV template described above.

Source token ratchet — down in all three scopes

scope before after change ceiling headroom
business semantics 81,233 80,767 −466 85,000 4,233 (5.2%)
interaction layer 39,084 38,848 −236 42,000 3,152 (8.1%)
authored total 134,179 133,465 −714 140,000 6,535 (4.9%)

The re-anchor advisory did not fire — it triggers above 10% headroom and no scope reaches it — so no ceiling is lowered here. Worth knowing why the drop is modest: each removed declaration left a short comment saying why the field is absent, and the ratchet is comment-stripped by design, so the numbers move by the declarations only.

Not run locally, left to CI

codeql, e2e (Playwright — grep confirms no spec references any removed field or the low_stock view), link-check, changeset-check (this PR adds .changeset/decorative-field-sweep.md, minor, with per-removal migration notes).

docs-app deserves a note: it is the only job that compiles content/docs, and it triggers only on apps/docs/** — so this PR's 27 MDX files will not be compiled by CI. That gap is already filed as #1169 and is not mine to fix here. Since CI will not check them, every edited page was structurally validated locally (frontmatter present and closed, code fences balanced, no ragged markdown tables) and all internal links I introduced match link forms already in use on neighbouring pages.

Scope

Held to the ten rows. crm_account.health_score was not touched (#1186). #1180's task_do_not_call_guard and event_do_not_call_guard in src/objects/task.hook.ts / event.hook.ts are untouched — crm_task was edited only where the two hour fields reached.

Filed out of scope, unassigned:

Tests

test/decorative-field-sweep.test.ts (new, 34 tests) pins both halves:

  1. every removed field stays removed across every surface that read it — object, views, all four locale bundles, seed, import mapping and CSV template — plus low_stock and its switcher tab asserted together, since a tab pointing at a deleted view and a view unreachable from any tab are two different broken states;
  2. parent_account still has the consumer it was kept for, and that consumer still works — the four-leg lifecycle above, re-measured against the booted stack on every run.

Half 2 is the half worth arguing for. A test that only asserted child_account_revenue is declared would pass just as happily on the day the platform stopped computing it — which would leave the enforced row in exactly the state nine other fields were removed for being in. The enforce verdict is only true while the engine keeps its side, so the test measures rather than reads.

Every absence assertion carries a positive control, and two of them earned their keep during this work: an accessor typo (.fields for .fieldMapping) made both import-mapping assertions vacuous, and the controls turned red rather than letting expect([]).not.toContain(...) pass over an empty array.

Generated by Claude Code


Generated by Claude Code

…each (#1182)

Ten declared fields had no business consumer. Nine are removed; one is kept and
given the roll-up it always claimed to have.

Removed, with every reader that went with them — view columns, filters and
sorts, form sections, seed values, all four locale bundles, the contact import
mapping AND its shipped CSV template, the developer field lists and the user
docs in all three locales:

  crm_product.quantity_on_hand / reorder_point  (+ the whole low_stock view,
    its switcher tab, and a "Low Stock" filter that compared against a
    hardcoded 10 rather than the reorder point beside it)
  crm_product.is_taxable, billing_type, unit_of_measure (+ 26 seeded values)
  crm_case.customer_signature, parent_case
  crm_task.estimated_hours, actual_hours
  crm_contact.reports_to, birthdate
  crm_campaign.parent_campaign

Kept and ENFORCED: crm_account.parent_account. The accounts docs promised
"roll-up reports — annual revenue of the global parent sums all children" and
nothing computed it. child_account_revenue is that promise: a Field.summary
over the self-referencing lookup, measured on the real engine across insert,
child update, re-parent and delete before being declared. The same docs claimed
cascading sharing, which crm_account (private) has never done; that claim is
now corrected rather than half-fixed.

Removal of published fields is authorized by the maintainer ruling of
2026-08-17, 逐个 enforce-or-remove(推荐), quoted in full in the PR body.

test/decorative-field-sweep.test.ts pins both halves: the removals stay removed
across every surface, and the roll-up is re-measured against the booted stack
so a platform regression cannot turn the enforced row back into a decoration.

Claude-Session: https://claude.ai/code/session_01XeAz8tSjwAibjdSDn9niiZ

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 17, 2026 4:12am

Request Review

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

Labels

ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decorative-field sweep: enforce-or-remove every declared field with zero business consumers

2 participants