Skip to content

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

Closed

Description

@os-steve

Problem

A full-repo consumer scan (main @ 2342811; a field counts as consumed only when something outside views/ and translations/ reads or writes it — rendering is not consumption) found these declared-but-inert fields:

Field(s) Only referenced by Suggested verdict
crm_product.quantity_on_hand, reorder_point object + view remove — no inventory logic exists or belongs here
crm_product.is_taxable object + view remove — crm_quote_line_item.tax_rate never reads it
crm_product.unit_of_measure, billing_type object + seed remove or enforce — seeded but read by nothing
crm_case.customer_signature object + view remove
crm_task.estimated_hours, actual_hours object + view remove — no rollup, no variance analysis
crm_contact.reports_to object + view remove — org chart declared, never traversed
crm_contact.birthdate object + view + import mapping remove or enforce (a birthday automation would be the only honest keep)
crm_account.parent_account object + view + import mapping enforce (one rollup) or remove
crm_campaign.parent_campaign object + view enforce (hierarchy ROI rollup) or remove
crm_case.parent_case object + view enforce (child-case rollup/related handling) or remove

Handled elsewhere, not in scope here: do_not_call (#1180, enforce), renewal_owner/next_renewal_date (#1181). Deliberate keep: crm_account.health_score — labeled "CSM-maintained", human-curated by design (but see the churn-report finding).

Why this matters more in this repo than in a normal app

HotCRM is the template every marketplace app forks; every decorative field here is a pattern the next AI author copies. The platform's own ADR-0049 doctrine is enforce-or-remove — the reference app should be its cleanest demonstration. Declared-but-inert keys are also permanent maintenance obligations (every one participates in imports, translations, and future migrations).

Ruled

Maintainer ruling, 2026-08-17, PM session chat (verbatim): 「逐个 enforce-or-remove(推荐)」, choosing the option described as: per-field adjudication in one card — strengthen do_not_call, give the three parents one rollup each or drop them, and delete the product inventory/tax surface, customer_signature, and the hour fields. Removal of published fields is normally a human-floor decision; this ruling is that decision — quote it in the PR body.

Dispatch notes

  • One PR, one dev: the rows touch many of the same object/view files; splitting invites same-file serialization for no benefit.
  • For each remove: field + view rows + seed values + all four translation-bundle keys + import-mapping entries go together; the build must stay green with no orphan keys.
  • For each enforce verdict on the parents: the minimal honest consumer is one rollup or one related-list behavior — if it balloons into feature design, stop and report needs_decision for that row only, land the rest.
  • Premise re-check per row before touching it: grep -rn "<field>" src/ --include=*.ts | grep -v views/ | grep -v translations/ — any row with a real consumer by dispatch time drops out of scope.

Activity

  1. added
    metadataDeclarative metadata — schema, security posture, UI surfaces
    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 Aug 17, 2026
  2. self-assigned this
    on Aug 17, 2026
  3. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 2
    Session: session_01XeAz8tSjwAibjdSDn9niiZ
    Branch: claude/issue-1182-decorative-field-sweep
    Worktree: hotcrm-issue-1182
    Domain: metadata
    File surface: src/objects/{product,case,task,contact,account,campaign}.object.ts, the matching src/views/*.view.ts, src/data/*.seed.ts, all four src/translations/*.ts, src/mappings/*, docs/, content/docs/**, test/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: batch 1 is fully merged — #1189 (#1181, d038b957), #1190 (#1183, 8ecc0bff), #1192 (#1180, c83aa744) — so the account.object.ts / account.view.ts contention that held this card is gone. #1180's new task_do_not_call_guard and event_do_not_call_guard now live in src/objects/task.hook.ts and event.hook.ts; this sweep must not disturb them. No other card is in flight. #1184 (comment slimming) is held behind this one because it edits the same object files. #1187 (README figures) is blocked on this card, because removing fields moves the published headline number.


    Generated by Claude Code

  4. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 1182,
      "status": "done",
      "branch": "claude/issue-1182-decorative-field-sweep",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1195",
      "premise_still_valid": true,
      "summary": "Ten rows adjudicated one by one, each with its own verdict and justification in the PR body: nine removed, one enforced. Before judging the three parent hierarchies I measured whether the ruling's 'one rollup' is actually available, because the answer decides which verdict is honest — Field.summary() is engine-computed and works self-referentially on the installed 17.0.0 across all four lifecycle legs (insert 3500, child update 6500, re-parent 4000, delete 0). That put enforcement at one declaration, so cost could not decide the parents and business need had to. crm_account.parent_account was ENFORCED (child_account_revenue): it is the only row whose exact rollup was already a published promise in three locales AND is importable today, so customer data arrives with no reader. parent_campaign and parent_case were REMOVED: campaign hierarchy has zero mentions anywhere and a rolled-up ROI is not one declaration (it would put two differently-scoped ROI numbers on one record, the two-truths defect #1189 removed), and a child-case count would compute but nothing in the service model acts on case parenting, so it is the same decoration with arithmetic in it. Removed with every reader: field, view columns/filters/sorts (including the whole low_stock view and its tab), form sections, seeds, all four locale bundles, import mapping AND its shipped CSV template, api_reference.md, STATUS.md (345->334), and 27 content/docs pages in all three locales. Three doc surfaces needed judgement rather than deletion: sales/accounts promised a rollup and cascading sharing and delivered neither (rollup now real, sharing claim corrected to the non-behaviour it is); sales/contacts described a clickable org tree and Copilot use that were both verified not to exist; service/cases carries exact field arithmetic that had to be recomputed (27/16/11/7/4). That last one forced ONE correction beyond my rows and I want it flagged: the cases 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), so subtracting two would have written a new false number — the missing entry is restored as the minimum edit that leaves the page true.",
      "tests": "pnpm verify (validate && typecheck && lint && lint:i18n-gate && hygiene && hygiene:tokens && build && test) — exit 0, 110 test files, 2680 passed, 1 skipped. Union run AFTER the final commit, at e16507d7. Plus `node scripts/check-stackblitz-lock.mjs` (the ci.yml gate verify omits, the #1149 gap): 'package-lock.json is in sync with package.json (v3)'. Control-byte self-scan over all changed files: clean. Token ratchet DOWN in all three scopes — business semantics 81,233->80,767 (-466), interaction layer 39,084->38,848 (-236), authored total 134,179->133,465 (-714); the re-anchor advisory did NOT fire (it needs >10% headroom; actuals are 5.2% / 8.1% / 4.9%), so no ceiling was lowered — the dispatch's 'take that advice if it fires' condition was not met. New test/decorative-field-sweep.test.ts (34 tests) pins both halves; half 2 boots the real stack and re-measures the roll-up lifecycle rather than asserting the declaration, because a declaration-only test would stay green on the day the platform stopped computing it. Two gates went genuinely red mid-work and neither was papered over: test/import-mappings.test.ts caught assets/import-templates/contacts.csv still carrying the Birthdate column (outside every src/-scoped scan — I had removed the mapping and the guide and missed the CSV; the new test now names that file), and the new test's own positive controls caught an accessor typo (.fields for .fieldMapping) that had made both import-mapping assertions vacuous. NOT run locally, left to CI: codeql, e2e (grep confirms no spec touches any removed field or low_stock), link-check, changeset-check. Note docs-app will NOT run on this PR: it is the only job that compiles content/docs but triggers only on apps/docs/**, already filed as #1169 — so all 27 edited MDX pages were structurally validated locally (frontmatter, fences, table shape) and every link form I introduced matches one already in use nearby.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #1193: crm_product.tax_rate is equally inert (nothing reads it; createLineItemPriceFill stamps list_price only) and the card's scan CANNOT see it — the token also matches crm_quote_line_item.tax_rate, a different object's field, so it reads as consumed. That is a false negative, invisible in the scan's output by construction; the issue carries the object-aware scan fix, which is the part with leverage for future sweeps.",
        "filed as #1194: content/docs/revenue/products lists three 'standard list views' that do not exist (Active Products, By Category, By Family) in all three locales; two of the five bullets named removed fields and went with them in this PR, the other three were already wrong. No guard covers view-name rosters — docs-drift counts objects/flows/dashboards/datasets/actions/positions, not views.",
        "NOT filed, already open as #1169: docs-app compiles content/docs but triggers only on apps/docs/**, so this PR's 27 MDX files get no CI compile. Searched before filing; #1169 covers it exactly."
      ]
    }

    Two standing-clause corrections for the PM, since the dispatch assumed otherwise:

    • scripts/pm/dispatch-gates.mjs does not exist in this repo (it is objectstack-repo tooling), so the affected-gate set was re-derived by hand from the actual diff paths after the final commit. The re-derivation added nothing beyond what pnpm verify covers except check-stackblitz-lock.mjs (run, green) and the docs-app gap noted above.
    • skip-changeset is not in play: this PR ships user-visible removals and adds .changeset/decorative-field-sweep.md (minor) with per-removal migration notes.

    Generated by Claude Code

  5. os-steve commented on Aug 17, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPT — PM review of #1195 (round 1). All 9 checks green including Build and Test (22.x). Verified against the branch and origin/main, not the report.

    Ten rows, ten verdicts, nine removals and one enforce — the per-field adjudication the ruling asked for, not a blanket sweep.

    The standout is the measurement that came before the judging. Rather than assume the ruling's "one rollup each" was affordable, the dev measured whether Field.summary() works on a self-referencing relationship on the installed 17.0.0, across all four lifecycle legs — insert, child update, re-parent, delete — and got the expected number every time. That turned enforcement into one declaration, which took cost off the table and forced each parent row to be decided on business need instead. Deciding what the deciding factor is, before deciding, is the part that is hard to do and easy to skip.

    Verified independently on the branch:

    The PM's scan was wrong a second time, and this correction is the more valuable of the two. #1193 reports that crm_product.tax_rate is equally inert but never reached this card, because the token also matches crm_quote_line_item.tax_rate — a different object's field. Re-run to confirm: of the three hits outside views//translations/, one is the product declaration and two belong to the line-item object, including the formula that reads its own rate. So the field has zero consumers and the scan reported it as consumed. That is a false negative, which a scan's output cannot show by construction — every field whose name is shared across objects has the same immunity, and is_taxable only reached the card because its name happens to be unique. The methodology fix in #1193 (resolve a hit to the object whose fields block it sits in) is the part with leverage, and it is now labelled finding for grading.

    Judgment calls worth recording:

    • parent_account enforce vs birthdate remove, both importable, separated on a sharp principle: volume of a declared write path is not pull; a stated purpose is. The account hierarchy had a published promise of that exact rollup in three locales plus a live import column with no reader; the birthdate column was collecting personal data toward a feature nobody had specified — and the dev named the privacy cost rather than only the maintenance cost.
    • parent_campaign rejected on consistency with refactor(account): remove the dead account-level renewal model #1189: a rolled-up ROI would put two differently-scoped ROI numbers on one record, which is the two-truths defect the earlier card removed. Refusing to build the thing you just deleted elsewhere is exactly the cross-card discipline this sweep needed.
    • parent_case rejected even though the rollup would compute — "a computed number nobody acts on is the same decoration with arithmetic in it." Correct: a count() that no mass-close, notification or SLA path reads is decoration with a number in it.

    Two gates went genuinely red mid-work and neither was worked around: test/import-mappings.test.ts caught assets/import-templates/contacts.csv still carrying the Birthdate column — outside every src/-scoped scan, which is the same class of blind spot as #1193 — and the new test's own positive controls caught an accessor typo (.fields for .fieldMapping) that had made two absence assertions vacuous. Absence assertions that cannot fail are the standard trap in removal work; the controls existing at all is why it surfaced.

    The service/cases correction deserves its own note. The page states exact field arithmetic and claims to account for every field on the object. It was already wrong before this PR — 28 stated against 29 declared, missing resolved_by_article — so subtracting the two removed fields would have written a new false number on top of an old one. Restoring the missing entry as the minimum edit that leaves the page true is the right call, and stopping to notice that at all is better.

    The one uncovered risk, and what I did about it. docs-app is the only job that compiles content/docs, and per #1169 it triggers only on apps/docs/** — so this PR's 27 MDX files get no CI compile. The dev disclosed this rather than letting it pass silently. I validated all 27 myself: frontmatter present and closed, code fences balanced, no ragged tables — clean. Combined with link-check green, that is adequate cover for this PR; #1169 remains the real fix.

    Token ratchet moved down in all three scopes (business 81,233 → 80,767, interaction 39,084 → 38,848, total 134,179 → 133,465). The re-anchor advisory correctly did not fire (needs >10% headroom; actuals 5.2% / 8.1% / 4.9%), so the dispatch's "take that advice if it fires" condition was not met and no ceiling was touched — the right reading of a conditional instruction.

    Flipping to ready and queueing.


    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

Labels

metadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions