Repository navigation
[Decision] crm_product.tax_rate: enforcing it is measurably wrong, so the remaining option is removal — which needs a ruling #1198
Description
Activity
- addedmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacesneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Aug 17, 2026 Maintainer ruling (2026-08-17, recorded by PM session
session_01NYgmGheCzM6NrHZN436Cxf). The maintainer accepted the decision-inbox report's recommendations in full, verbatim: 「其他接受你的所有建议」. For this card:Ruled: remove
crm_product.tax_rate.- Basis, as measured: enforcing it is provably wrong (the app's two tax models — per-line percentage formula vs quote-level manual amount — never read each other; a stamped rate yields quote totals 200 lower than the sum of line totals and double-taxes any rep who also fills
crm_quote.tax); the field has zero consumers, zero UI affordance, zero seeded values, and the docs already promise nothing for it. - Removal scope per the card's own costing: the field, four locale rows, the docs row in three locales, the
api_reference.mdfield list. Published-field deletion authorized explicitly by this ruling; no in-repo data is affected (all 13 catalog products at default0). - The third option (product-rate-driven quote tax) is explicitly NOT taken — a real tax model is a future product-design card if ever wanted, not part of this verdict; nothing in this removal forecloses it.
Label flipped
needs-user-decision→pm:queuein the same stroke.
Generated by Claude Code
- Basis, as measured: enforcing it is provably wrong (the app's two tax models — per-line percentage formula vs quote-level manual amount — never read each other; a stamped rate yields quote totals 200 lower than the sum of line totals and double-taxes any rep who also fills
- addedpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Aug 17, 2026 - added a commit that references this issue
on Aug 24, 2026 huangyiirene commented
on Aug 31, 2026 CollaboratorMore actions既裁恢复:2026-08-17 的删除裁决仍然有效 —— 8/22 的重升级系未读裁决记录所致,本卡退出决策箱(不占新裁决)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· hotcrm 决裁批 #18 盘点项按「重挂前先查裁决」纪律核验
- 评论 5315744424(2026-08-17)是一条完整的裁决记录:维护者逐字引文(「其他接受你的所有建议」)+ 明确范围(字段/四语言行/三语言文档行/api_reference 列表)+ 「Published-field deletion authorized explicitly by this ruling」 + 同笔转
pm:queue; - 评论 5381231651(2026-08-22)把标签退回
needs-user-decision,理由是「删除已发布字段是人工地板」—— 但该评论通篇未提及、未质疑上一条裁决记录:地板问题在 8/17 已由维护者亲自跨过,重升级是未读全记录的程序失误(本车道已知失败模式),不构成对裁决的推翻; - 8/22 评论补充的唯一新事实(「外部客户是否写过该字段仓内不可知」)在 8/17 裁决中已被权衡并明确授权,不构成新证据。
执行
裁决原样恢复:删除
crm_product.tax_rate,范围照 8/17 记录;第三选项(产品税率驱动报价税额)照旧明确不采、不被本删除关闭。needs-user-decision→pm:queue(同笔)。⚠️ 透明记录:若维护者今日想推翻 8/17 裁决,一行即改;本评论只恢复既裁,不产生新裁。
Generated by Claude Code
- 评论 5315744424(2026-08-17)是一条完整的裁决记录:维护者逐字引文(「其他接受你的所有建议」)+ 明确范围(字段/四语言行/三语言文档行/api_reference 列表)+ 「Published-field deletion authorized explicitly by this ruling」 + 同笔转
- addedpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Aug 31, 2026 - addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Sep 3, 2026 Claim:
repo:hotcrmPM seat — dispatching to anos-devagent now (round R32).- Session:
session_019hUuCQStzXGMFSX4dzww5t - Branch:
claude/issue-1198-product-tax-rate-removal - Ruling being executed: the 2026-08-17 deletion ruling on this card. The 8/22 re-escalation was a procedural failure (the full record was not read); the deletion ruling stands and is what is being carried out.
- File surface (declared, exclusive for this claim):
src/objects/product.object.ts(thetax_ratefield)src/objects/quote_line_item.object.ts(only if it references the field — measure first)src/translations/{en,es-ES,ja-JP,zh-CN}.ts(the four language rows)content/docs/revenue/products.mdx,.zh-Hans.mdx,.zh-Hant.mdx(the three doc rows)docs/feature-inventory.mdsrc/data/revenue.seed.ts(only if it seeds the field)- a changeset under
.changeset/
- ⛔ Fenced out of surface:
docs/developers/api_reference.md. It is insideTREE_DOCSand it is the declared target of docs/README.md still orders maintainers to hand-copy object fields into docs/developers/api_reference.md #1494 (queued, undispatched). If the api_reference list carries atax_raterow, report it, do not edit it — the seat will route it. - Clause-②:
src/translations/**is a hot serial file. This claim holds it, which fences Orphaned locale keys are invisible to every i18n check — the gate only looks surface→translation #1262 / The 70% advisory band's first real output:es-ES.ts(75.3%) andja-JP.ts(73.4%) are the only two files it names, and a third sits 224 bytes below it #1311 / All four locale files translate acrm_caseform sectionsla_overviewthat was deleted with the SLA/Resolution sections — 4objectstack validatewarnings, heading stays in the source locale #1511 / The object-aware scan surfaces 14 more inert fields, 5 of them masked by a shared name — a second enforce-or-remove ledger #1199 for the duration. Touch only thetax_raterows; any other edit in those files belongs to a fenced card. - ⛔ Explicitly not in scope: the third option (product tax rate driving quote tax). It is not taken and is not closed by this deletion.
Generated by Claude Code
- Session:
os-dev-report
{ "issue": 1198, "status": "blocked", "branch": "claude/issue-1198-product-tax-rate-removal", "pr": null, "premise_still_valid": true, "summary": "The runtime premise holds exactly as the card states: nothing computes with crm_product.tax_rate. But the deletion is NOT a pure removal, and it is not executable inside this claim's declared file surface. test/field-consumer-scan.test.ts pins the field as the canonical fixture of the #1193 object-aware-resolution guard, and I MEASURED the breakage by ablation rather than predicting it: removing the field and its four locale rows turns 4 test cases red (15 passed, 4 failed). The ruling's zero-consumers basis is sound but structurally blind here - scan-field-consumers.ts walks only the registered metadata stack (roots objects/views/pages/apps/translations/data/mappings), never test/, so a test fixture cannot appear in the ledger the ruling was read from. Repairing it is authorship, not a mechanical edit, and it collides with a card fenced behind this very claim: the guard needs a shared field name carrying BOTH a live and an inert verdict, there are exactly three on this tree (tax_rate, line_number, description), and #1199 proposes removing line_number and description. All 15 inert rows on the tree are under adjudication (1 here + 14 on #1199), so if both cards land as proposed, ZERO inert fields remain and the live-vs-inert fixture class is extinguished. I stopped before touching test/ and left the tree byte-identical. I also falsified three of the ruling's scope items (feature-inventory row, api_reference row, revenue.seed value): all three carry nothing to delete, while the doc surface is 4x larger than 'three doc rows'. Branch is pushed and empty; no commits.", "blocked_on": "PM decision, two parts. (1) May this claim's declared file surface be extended to test/field-consumer-scan.test.ts? The deletion cannot leave pnpm test green without editing it, and that file is #1193's artifact, outside the surface the claim comment declared exclusive. (2) Which replacement fixture may the #1193 guard use, given #1199 targets the obvious candidates? Recommendation and durable option in open_questions. Everything else is measured and staged - a follow-up round is pure execution.", "tax_rate_hit_list": { "method": "git grep -n over 872 tracked files. Positive control: list_price returns 125 hits. Negative control: zzz_definitely_absent_token returns 0. So a zero result means absence, not a broken pattern. Total: 44 hits (41 outside CHANGELOG.md, 3 inside it).", "would_remove_5_files_13_lines": [ "src/objects/product.object.ts:154-162 - the '// Tax & billing' comment plus the tax_rate field block (the field itself)", "src/translations/en.ts:891 - tax_rate: { label: 'Default Tax Rate %' } (inside the crm_product block opening at 862)", "src/translations/es-ES.ts:917 - label 'Tasa de Impuesto por Defecto %' (crm_product block opens 890)", "src/translations/ja-JP.ts:881 - label the default-tax-rate wording (crm_product block opens 854)", "src/translations/zh-CN.ts:928 - label the default-tax-rate wording (crm_product block opens 898)" ], "would_remove_docs_12_sites_not_3": [ "content/docs/revenue/products.mdx:47 + zh-Hans:47 + zh-Hant:47 - the 'Default Tax Rate %' table row (the 3 rows the ruling counted)", "content/docs/revenue/products.mdx:51 + zh-Hans:51 + zh-Hant:51 - a whole PROSE PARAGRAPH about the product rate not being applied; dies with the field", "content/docs/revenue/products.mdx:3 + zh-Hans:3 + zh-Hant:3 - frontmatter description ends 'SKUs and tax rates'", "content/docs/revenue/products.mdx:17 + zh-Hans:17 + zh-Hant:17 - the Pricing field-group summary row lists 'tax rate'" ], "left_standing_and_why": [ "src/objects/quote_line_item.object.ts:142,159 - crm_quote_line_item declares its OWN live tax_rate, read by the total_price formula. Different object. NOT this card.", "src/objects/quote_line_item.object.ts:57,60 - comments. Line 60 says 'crm_product keeps tax_rate there too' - a cross-reference that goes STALE on deletion. One clause, in another object's file. Reported, not edited.", "src/data/revenue.seed.ts:223 - a COMMENT only, and it is about crm_quote_line_item.tax_rate, inside the quoteLineItems seed block. Seeds no product value. Must not be touched.", "src/translations/{en,es-ES,ja-JP,zh-CN}.ts:{1308,1338,1283,1268} - the OTHER four tax_rate locale rows, belonging to crm_quote_line_item. Naive grep counts 8 rows; only 4 are the product's.", "test/undeclared-key-probe.test.ts:41,83,256 - uses tax_rate as a key declared on crm_quote_line_item and absent from crm_opportunity_line_item. crm_product is not involved. Unaffected by the deletion.", "test/line-item-conventions.test.ts:128 - evaluates the quote line item's own formula. Unaffected.", "scripts/scan-field-consumers.ts:16,17,194,489 - narrative comments recording the #1193 history. Accurate history, not a reference to the live declaration.", "content/docs/revenue/products.mdx:95 (+ both Chinese faces) - 'Tax is not filled in: enter it on the quote' is about QUOTE-level tax and stays true after the deletion.", "CHANGELOG.md - 3 hits, historical release notes, never retro-edited.", "test/field-consumer-scan.test.ts - 17 hits. THE BLOCKER. See test_fixture_blocker." ] }, "test_fixture_blocker": { "measured_by": "ablation, not prediction", "failing_cases_4_of_19": [ "'crm_quote_line_item.tax_rate is live and crm_product.tax_rate is not' - AssertionError: expected false to be true (fieldsByObject.get('crm_product').has('tax_rate'))", "'the inert one has locale rows and nothing else' - AssertionError: expected [] to include 'translations'", "'a DECLARED but genuinely inert field is still reported, still exit 0' - AssertionError: expected false to be true; the --sites spawn would now exit NON-ZERO (the #1255 refusal path)", "'names the objects that do declare the field, when any do' - expected the near-miss line to contain \"'tax_rate' is declared on crm_product, crm_quote_line_item\"" ], "vitest_summary": "Tests 4 failed | 15 passed (19)", "why_the_repair_is_not_mechanical": "The first describe block exists to prove the resolver can tell two same-named fields apart - a failure mode the file's own header calls invisible ('a false negative cannot appear in the scan's own output'). crm_product.tax_rate IS that fixture, narrated throughout the file's prose as '#1193's headline row' and 'the control'. Re-pointing it picks a new fixture, rewrites the explanatory comments, and re-specifies the --sites control - authorship in another card's guard.", "cross_card_collision": "Shared field names carrying both a live and an inert verdict, measured from pnpm scan:fields --json: exactly three - tax_rate, line_number (crm_opportunity_line_item live / crm_quote_line_item inert), description. #1199 (pm:queue, fenced behind this claim) proposes removing crm_quote_line_item.line_number AND the three inert description rows. So the obvious replacement fixture is on the next card's chopping block; choosing it would silently pre-empt #1199's adjudication.", "timeline_dated_via_rest_not_the_shallow_clone": "The local clone is shallow (59 commits, floor 2026-08-31) and would have mis-dated this file; dated through the REST commits endpoint instead. 5d46177f 2026-08-17T05:46:24Z created the scan and the fixture, 6h14m BEFORE the ruling comment at 2026-08-17T12:00:39Z. ac02bf15 2026-08-24T02:45:30Z (#1266) then DEEPENED the dependency, adding the --sites control that pins crm_product.tax_rate by name with exit 0 and the literal output string - seven days AFTER the ruling." }, "zone2_measurements": { "1_runtime_consumers": "HOLDS at runtime, FALSIFIED as a pure removal. No hook, flow, view, or formula reads the product's rate - the only formula reading a tax_rate is crm_quote_line_item.total_price reading its own. But 4 test cases depend on the declaration. Note the scan the ruling relied on cannot see test/: CARRIER_ROOTS/DISPLAY_ROOTS and the walk cover the registered metadata stack only.", "2_counts": "Locale rows: 4 for crm_product (CORRECT), out of 8 tax_rate locale rows total - the other 4 are crm_quote_line_item's. Doc table rows: 3 (CORRECT), but the doc surface is 12 sites across the 3 faces (row + prose paragraph + frontmatter description + Pricing group summary). No fifth locale pack, no fourth doc face.", "3_validate": "HOLDS. Ablated tree: '✓ Validation passed (1718ms)', exit 0, 54 warnings. Base tree: '✓ Validation passed (1566ms)', exit 0, 54 warnings. Warning sets identical; none mentions tax_rate or crm_product. POSITIVE CONTROL that validate really re-read the mutated tree: 'Data: 18 Objects 334 Fields' (base) vs 'Data: 18 Objects 333 Fields' (ablated) - exactly one field fewer.", "4_locale_parallelism": "HOLDS. After removing the four rows, all four packs carry 21 crm_product field keys and the key sets are IDENTICAL (symmetric difference empty against en). pnpm lint:i18n-gate exit 0: '✓ i18n lint gate: 0 i18n/missing-* issues (59 total lint issue(s) reported, unaffected by this gate)'.", "5_revenue_seed": "FALSIFIED, usefully. src/data/revenue.seed.ts seeds NO product tax_rate. Its single tax_rate occurrence is line 223, a COMMENT inside the quoteLineItems seed block explaining why the QUOTE LINE ITEM's rate is left at 0: 'Seeding a rate here would put a second, contradictory tax number on the record.' The file is out of surface and must not be edited." }, "api_reference_reported_not_edited": "docs/developers/api_reference.md carries NO tax_rate row and no crm_product field list - zero occurrences of 'tax_rate', and zero of 'tax' in any spelling. The file is 1305 bytes and now says the field lists are 'deliberately not restated here', citing the 2026-08-31 ruling item 5; #1488 removed the fifteen hand-copied 'Key fields:' lists. So the ruling's scope item 'the crm_product field list in api_reference.md' was already satisfied by another card, and the #1494 fence has nothing to protect on this issue. NOT EDITED - I did not open it for writing.", "verify_outcome": "pnpm verify NOT run to completion: there is no implementation to verify - the tree is unmodified at origin/main f0a05613. Components exercised individually during the ablation, all on the mutated tree: pnpm validate exit 0, pnpm lint:i18n-gate exit 0, vitest test/field-consumer-scan.test.ts exit 1 (4 failed | 15 passed). Exit codes captured by redirecting to a file FIRST and reading the status before any pipe, so no head/tail could rewrite them.", "tests": "ABLATION (the only measurement run; no implementation exists). No rebuild step applies and none is claimed: the subject resolves through SOURCE, not dist - the test imports '../scripts/scan-field-consumers' directly and the --sites cases spawn tsx on scripts/scan-field-consumers.ts, while pnpm validate loads objectstack.config.ts through tsx. There is no exports/dist hop for a stale artifact to hide in. MUTATION CONFIRMED ON DISK, not by the editor's exit code: after deleting the field block and the four product locale rows, grep -c 'tax_rate' src/objects/product.object.ts = 0, grep -c 'Default Tax Rate %' = 0 in both product.object.ts and en.ts, and the SURVIVOR probes held - quote_line_item.object.ts still 4 hits, each locale pack down from 2 tax_rate rows to exactly 1. git diff --stat: 5 files changed, 13 deletions(-). RESTORE LEG PROVEN, not assumed: restore was 'git checkout HEAD -- ABSOLUTE_PATH' per file (HEAD named explicitly so a polluted index cannot hand back the mutation), driven by a trap on EXIT INT TERM with REPO_ROOT resolved before mutating. Verified by state, not exit code - all five git hash-object values match their HEAD blobs (product.object.ts 2d220399, en.ts c20d1368, es-ES.ts 681136a6, ja-JP.ts cdd60763, zh-CN.ts f59ac2fe), 'git diff HEAD' empty, 'git status --porcelain' empty, and the field is back (product.object.ts 1 hit, en.ts 2 hits). RESULTS: validate exit 0 with 333 vs 334 fields (control proving the re-read); lint:i18n-gate exit 0 with 0 missing-key issues; field-consumer-scan.test.ts exit 1, 'Tests 4 failed | 15 passed (19)'.", "mcp_calls": "2 - one issue_write (filed #1536) and one add_issue_comment (this report). Every GitHub READ went through the unauthenticated REST channel; note the /search/ endpoints are closed to this session ('sessions are bound to their configured repositories'), so issue dedup used the repo-scoped issues listing plus a local grep.", "open_questions": [ { "question": "May this claim's declared file surface be extended to test/field-consumer-scan.test.ts, so the deletion can land green?", "options": [ "A - Extend the surface to that one file in this claim, repair the fixture, land the deletion in one PR.", "B - Keep the surface as declared; land the deletion and the fixture repair as a paired change owned by whoever owns #1193's guard.", "C - Sequence #1198 behind #1199 so the fixture is re-pointed once, after the full inert ledger is adjudicated, instead of twice." ], "recommendation": "C, then A. Long-term soundness dominates: all 15 inert rows are under adjudication right now (1 here + 14 on #1199), so re-pointing the fixture today means re-pointing it again when #1199 lands - and #1199 targets both obvious replacements. Deciding the whole ledger first makes it one edit with the final field population known. If the deletion must land now, A with the durable fixture in the next question - but A alone, chosen without #1199 in view, is what produces the second churn." }, { "question": "Which replacement fixture should the #1193 discrimination guard use, given every live-vs-inert shared pair is on a chopping block?", "options": [ "A - crm_quote_line_item.line_number (live/inert twin pair, structurally identical to tax_rate). FRAGILE: #1199 proposes removing it.", "B - A live-vs-DISPLAY-ONLY shared pair, e.g. description with crm_account live and crm_product display-only. Both survive both cards.", "C - Keep crm_product.tax_rate solely as a test fixture and do not delete it." ], "recommendation": "B. Measured: after both cards land as proposed, zero inert fields remain, so every live-vs-inert pair is extinguished and A cannot be durable. Seven shared names keep a live-vs-display-only divergence through both cards (billing_address, description, do_not_call, lead_source, notes, title, website), so B still guards the real thing - that the resolver answers per object rather than per token. Two consequences the seat should decide with open eyes: the guard's boundary changes from live-vs-inert to live-vs-display-only, and the --sites control case, which by its own comment needs 'a DECLARED but genuinely inert field' returning exit 0, has no inert field left to pin and must be re-specified. C is rejected on the startup-focus axis and on the four-axis reading the ruling already made: a regression fixture is not a business consumer, and letting a test fixture veto a maintainer ruling inverts the hierarchy. The fixture cost is real but it is payable." } ], "out_of_scope_findings": [ "filed as #1536: docs/feature-inventory.md QUO-010 (line 119) credits crm_product with 计费类型/计量单位/库存 - billing_type, unit_of_measure and any inventory field are all absent from the object's 14 declared fields - and QUO-012 (line 122) names a low-stock view that src/views/product.view.ts:11-13 itself records as removed. Three stale claims in two adjacent rows. Dedup: 0 matches across 83 open issues.", "NOT filed, reported for routing: src/objects/quote_line_item.object.ts:60 says 'crm_product keeps tax_rate there too', which becomes false the moment this deletion lands. One comment clause in a file this claim may only touch if it references the field. Whoever executes the deletion should fix it in the same PR - flagging it so it is not missed rather than filing a card for one clause.", "NOT filed, observation for the seat: the 2026-08-17 ruling's removal scope named an api_reference.md field list that #1488 had already deleted, a feature-inventory row that never existed, and undercounted the doc surface 3 sites vs 12. Nothing to fix in the repo - but if scope statements in decision cards are reused as dispatch surfaces, they are worth re-measuring at dispatch time rather than trusting." ] }
Generated by Claude Code
- added and removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Sep 3, 2026 Seat disposition — claim released, card blocked on #1543.
The R32 dispatch came back with no PR and the tree byte-identical. That is the right outcome and the seat is recording it as a delivered result, not a failed run: the deletion cannot leave
pnpm testgreen without editingtest/field-consumer-scan.test.ts, which is #1193's artifact and was outside the surface this claim declared exclusive.⛔ The deletion ruling is not reopened. The blocker is what happens to #1193's guard afterwards — see #1543.
The seat verified the load-bearing half of the report independently rather than accepting it:
test/field-consumer-scan.test.tscarries 19tax_rateoccurrences;:262pinscrm_product.tax_rate — 4 site(s)as a literal output string and:304pins the near-miss line by name; andscripts/scan-field-consumers.ts:123/144confirm the scan's roots aretranslations/data/mappings+views/pages/apps—test/is not among them, which is why a test fixture could never have appeared in the ledger the ruling was read from.State changes:
- this card →
pm:blocked, assignee cleared, branchclaude/issue-1198-product-tax-rate-removalpushed empty atf0a05613 - The object-aware scan surfaces 14 more inert fields, 5 of them masked by a shared name — a second enforce-or-remove ledger #1199 →
pm:blockedon the same card — it removes both replacement fixtures, so it hits this wall from the other side - the
src/translations/**serial fence is released; Orphaned locale keys are invisible to every i18n check — the gate only looks surface→translation #1262 and All four locale files translate acrm_caseform sectionsla_overviewthat was deleted with the SLA/Resolution sections — 4objectstack validatewarnings, heading stays in the source locale #1511 are free again
Carried forward to whoever executes this once #1543 rules — three scope items in the 2026-08-17 ruling are stale (the
feature-inventory.mdrow never existed; theapi_reference.mdlist was already removed by #1488, so the #1494 fence has nothing to protect here;revenue.seed.tsseeds no product rate and must not be touched), the doc surface is 12 sites, not 3, andsrc/objects/quote_line_item.object.ts:60carries a cross-reference that goes false on deletion and should be fixed in the same PR.Separately filed from this run: #1536 (
feature-inventory.mdQUO-010/QUO-012 creditcrm_productwith three fields it does not declare, and name a viewsrc/views/product.view.tsrecords as removed).
Generated by Claude Code
- this card →
Pointer (director seat, summon #15, 2026-09-05T11:3xZ): #1543 is ruled F by the maintainer — the field-consumer scanner and its guard are retired from this repo outright (comment 5552508432 on that card), so the fixture collision this card was blocked on disappears rather than being re-pointed. This card stays
pm:blockedon #1543 until the retirement PR is MERGED (deletingcrm_product.tax_ratebefore the guard is gone turns four of its assertions red); it is released in the same stroke afterwards and dispatched under its existing 2026-08-17 ruling, carrying the four stale scope items #1543 recorded (feature-inventory row absent, api_reference list already removed by #1488, the seed file'stax_rateis a comment only, the doc surface is 12 sites across 3 faces) plus thequote_line_item.object.ts:60comment clause.
Generated by Claude Code
- addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removed
on Sep 5, 2026 Claim: PM loop round — director seat summon #15; the #1543 blocker is gone (PR #1580 MERGED 2026-09-05T14:58:05Z, hotcrm
main021db549no longer carriesscripts/scan-field-consumers.tsortest/field-consumer-scan.test.ts); dispatched as the release step the F ruling planned (5552508432 item 3), under the maintainer's 2026-09-05 direct-dispatch instruction on #1543 (「你现在就派发处理」); therepo:hotcrmseat #1353 is vacant
Session:session_01TezFG8ZMrNH6n5VTNpPpdH
Branch:claude/issue-1198-product-tax-rate-removal-2(the R32 branchclaude/issue-1198-product-tax-rate-removalis an empty probe atf0a05613; a fresh branch from today'smainavoids a stale base)
Worktree:hotcrm-issue-1198
Domain:repo:hotcrm
Ruling executed: 2026-08-17 removecrm_product.tax_rate(5315744424, maintainer verbatim 「其他接受你的所有建议」; restored 5480898648 after the 08-22 re-escalation was found to have been made without reading the record). The third option (product rate driving quote tax) is NOT taken and NOT foreclosed.
File surface (measured onmain021db549, 15:00Z):src/objects/product.object.ts(thetax_rateblock at ~:154-162with its// Tax & billingcomment) ·src/translations/{en,es-ES,ja-JP,zh-CN}.ts— thecrm_productblock'stax_raterow only (en:891,es-ES:1021,ja-JP:985,zh-CN:1051); ⛔ the fourcrm_quote_line_itemrows (en:1308,es-ES:1506,ja-JP:1451,zh-CN:1436) stay ·content/docs/revenue/products.mdx+.zh-Hans.mdx+.zh-Hant.mdx— 12 sites across the 3 faces (table row, the prose paragraph about the unapplied rate, the frontmatter description's "tax rates", the Pricing field-group summary row); ⛔ the "Tax is not filled in: enter it on the quote" sentence is about quote-level tax and stays ·src/objects/quote_line_item.object.ts:60— the one comment clause "crm_product keeps tax_rate there too" · one changeset. ⛔ Untouched:src/data/revenue.seed.ts(its onlytax_rateis a comment about the line item),docs/developers/api_reference.md(#1488 already removed the field lists; the #1494 fence protects nothing here),docs/feature-inventory.md(no such row; #1536 is a separate card),test/undeclared-key-probe.test.tsandtest/line-item-conventions.test.ts(about the line item's owntax_rate), governed surfaces (stop on breach; explain in the report)
Container & model: S metadata deletion with a 12-site doc surface, handled as M;mode:subagent;model: opus—--tier(objectstack dispatch-gates) has no hotcrm path mandate; default tier because the docs faces carry two locale conventions (#1329 / #1368) a deletion must not disturb
Clause-②: no (an app's published field is removed under an explicit maintainer ruling; no platform contract changes; the app-level changeset states the removal)
Serial constraints cleared: this claim holds thesrc/translations/**fence — #1262 · #1311 · #1511 · #1199 are fenced for its duration (all four unclaimed at 15:00Z; open hotcrm PRs are two dependabot bumps); #1199 additionally stayspm:queueand undispatched pending a maintainer ruling on its 14 published-field rows; none otherwise.
Generated by Claude Code
Dev claim —
os-devsubagent,mode:subagent, dispatched by the PM claim above (5552666138).- Session:
session_01TezFG8ZMrNH6n5VTNpPpdH - Branch:
claude/issue-1198-product-tax-rate-removal-2(pushed empty at021db549as the landing marker; the R32 branchclaude/issue-1198-product-tax-rate-removalis untouched) - Worktree:
/home/user/hotcrm-issue-1198, base021db549— the commit that merged chore(test): retire the field-consumer scanner and its guard — detection belongs to the platform (#1543) #1580 and retiredscripts/scan-field-consumers.ts+test/field-consumer-scan.test.ts, i.e. the blocker R32 hit is gone from the tree I am working on. - Ruling executed: 2026-08-17 remove
crm_product.tax_rate(5315744424, restored by 5480898648). The third option (product rate driving quote-level tax) is NOT taken and NOT foreclosed; it gets one sentence in the changeset and nothing else. - File surface (exclusive for this claim):
src/objects/product.object.ts·src/translations/{en,es-ES,ja-JP,zh-CN}.ts(thecrm_productblock'stax_raterow ONLY — I will print the enclosing object key before each deletion) ·content/docs/revenue/products.mdx+.zh-Hans.mdx+.zh-Hant.mdx·src/objects/quote_line_item.object.ts(the one stale comment clause at:60) · one.changeset/*.md. - Fenced out, will not open for writing:
src/data/revenue.seed.ts,docs/developers/api_reference.md,docs/feature-inventory.md,test/undeclared-key-probe.test.ts,test/line-item-conventions.test.ts,CHANGELOG.md, existing changesets, and the governed surfaces (AGENTS.md,CLAUDE.md,.claude/**,docs/adr/**, rootskills/**). - Serial fence honoured: the PM claim holds
src/translations/**for this card's duration; I touch only the fourcrm_producttax_raterows there.
Re-measuring the whole surface on
021db549myself before editing; anything that differs from the dispatch's 15:00Z measurement goes in the report.
Generated by Claude Code
- Session:
os-dev-report
{ "issue": 1198, "status": "done", "branch": "claude/issue-1198-product-tax-rate-removal-2", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1588", "premise_still_valid": true, "summary": "The 2026-08-17 deletion ruling is executed in full and the tree is green. crm_product.tax_rate is gone: the field block plus its '// Tax & billing' header in src/objects/product.object.ts, one row in each of the four locale packs, and the 12 doc sites across the three product-page faces. The R32 blocker is genuinely gone - main 021db549 carries neither scripts/scan-field-consumers.ts nor test/field-consumer-scan.test.ts (retired by #1580), so the fixture collision that stopped the last round no longer exists and no test needed repairing. Every carried-forward correction from R32 re-measured TRUE on today's main: the api_reference field list is absent (#1488 already removed it), docs/feature-inventory.md carries no such row, revenue.seed.ts:223 is a comment about the LINE ITEM's rate, and the doc surface really is 12 sites, not 3. One surface item the dispatch did not name turned up and is included: docs/STATUS.md:22 transcribes the validator summary and states the field total, so 334 becomes 333 - that is pin test/docs-declared-versions.test.ts (#1011) doing its job, and I proved it by ablation rather than assuming. Deviation: I also fixed a SECOND stale comment cross-reference the deletion makes false, in src/objects/opportunity_line_item.object.ts:63, which is outside the dispatched surface - declared below.", "gates": [ { "command": "pnpm validate", "exit": 0, "verdict": "✓ Validation passed (1473ms) · Data: 18 Objects 333 Fields" }, { "command": "pnpm typecheck", "exit": 0, "verdict": "tsc --noEmit, no diagnostics" }, { "command": "pnpm lint", "exit": 0, "verdict": "90 warning(s), 12 suggestion(s) — no errors" }, { "command": "pnpm lint:i18n-gate", "exit": 0, "verdict": "✓ i18n lint gate: 0 i18n/missing-* issues (102 total lint issues, unaffected by this gate)" }, { "command": "pnpm hygiene", "exit": 0, "verdict": "✓ source hygiene clean" }, { "command": "pnpm hygiene:tokens", "exit": 0, "verdict": "✓ source token ratchet clean — business semantics ~84,018 (ceiling ~85,000); interaction ~37,429 (ceiling ~40,000); authored total ~135,789 (ceiling ~140,000). NO ceiling raised; the deletion lowers the count." }, { "command": "pnpm build", "exit": 0, "verdict": "Artifact: dist/objectstack.json (2011.4 KB)" }, { "command": "pnpm test", "exit": 0, "verdict": "Test Files 159 passed (159) · Tests 3380 passed, 1 skipped (3381)" } ], "gate_method": "Each command run SEPARATELY in the foreground, output redirected to a file FIRST and the exit code read before any pipe (cmd greater-than log 2 greater-than and-1; EXIT=dollar-question), so no head/tail could rewrite a status. Verdict lines quoted are the gates' own printed lines, never a bare exit code. This repo has no scripts/pm/os-verify-lock.sh - that is an objectstack script; the chain here is package.json's own, which pnpm verify concatenates.", "pm_mechanism_assumptions": { "1_validate_one_field_fewer": "HOLDS, and it is the positive control that validate re-read the tree. Base 021db549 unmodified: exit 0, 'Data: 18 Objects 334 Fields'. Mutated tree: exit 0, 'Data: 18 Objects 333 Fields'. Today's baseline equals R32's, so the field total was 334 on both days.", "2_docs_pins": "ONE pin went red, for exactly the authorised reason, and it is not one of the three the dispatch guessed at. docs-metadata-counts / docs-object-coverage / docs-dashboard-tiles all stayed green: they match prose spellings (objects, flows, dashboards, datasets, actions, positions) and none of them counts FIELDS. The pin that carries the field total is test/docs-declared-versions.test.ts (#1011), which parses the validator transcript fenced in docs/STATUS.md and compares every figure against the registered stack. The figure it protects: 'Data: 18 Objects 334 Fields' at docs/STATUS.md:22, updated to 333. ABLATION PROOF that this is the pin working rather than me guessing: with the figure left at 334 the suite fails - 'AssertionError: docs/STATUS.md transcribes figures that are not the current ones', Tests 1 failed | 18 passed, exit 1; at 333 it is 19 passed. Restore verified BY STATE, not by an exit code: git hash-object docs/STATUS.md = 38ddb8f52170a1a45185300d910a5a8b32782cf9 = the HEAD blob, git diff HEAD empty, git status --porcelain empty. docs/STATUS.md:22 is the only site in docs/ or content/ stating 334.", "3_i18n_gate": "HOLDS. pnpm lint:i18n-gate exit 0, 0 missing-key issues. Measured directly as well: after the deletion all four packs carry IDENTICAL crm_product field key sets, symmetric difference against en empty, tax_rate absent from all four. My count calibre is 13 DIRECT field keys per pack (option sub-keys and _views not counted); R32's '21' counted on a different calibre, so the number differs while the parity claim - the load-bearing half - is the same.", "4_no_runtime_consumers": "HOLDS. git grep -n tax_rate over the tracked tree, with controls (positive: list_price hits 30 files; negative: a nonsense token returns exit 1), classified every one of the 22 pre-edit hits. Nothing under src/views, src/pages, src/apps, src/flows, src/hooks, src/data or src/mappings references the product's rate - those directories return ZERO tax_rate hits of any kind except the one comment in src/data/revenue.seed.ts:223, which is about the LINE ITEM. Post-edit survivors, all correct: quote_line_item.object.ts (its own live field at :141 and the total_price formula reading it at :158, plus a fieldGroup comment), the four crm_quote_line_item locale rows, revenue.seed.ts:223's comment, test/undeclared-key-probe.test.ts + test/line-item-conventions.test.ts (the line item's field), CHANGELOG.md history and one existing changeset." }, "deviations": [ "IN-SCOPE ADDITION (not in the dispatched surface, required by a pin): docs/STATUS.md:22, 334 → 333 Fields. Authorised by PM mechanism assumption 2 - a pin went red because the documented figure changed, so the documented figure moves. Pin and figure named above.", "IN-PLACE FIX (declared): src/objects/opportunity_line_item.object.ts:63 said 'crm_quote and crm_product both keep every money field — tax included — in pricing'. The deletion makes the crm_product half false. Re-attributed to crm_quote's tax, where it is true. Four conditions met: same defect class as the quote_line_item.object.ts:60 clause the dispatch already authorises, mechanical and comment-only, no other claim holds the file (the only open hotcrm PRs are two dependabot bumps), and no new verification surface. Reported here and named in the PR body rather than filed as a card for one clause.", "DOC PROSE, not a pure deletion: the paragraph at :51 on each face existed only to explain that the inert rate is not applied. Deleting it outright would drop a still-true business concept, so it is replaced by ONE sentence per face - tax is a quote-level figure, the line item has its own rate, the catalog stores nothing about tax. The three faces keep their own locale conventions; the zh-Hant page's English console navigation (#1368) and the '#95 Tax is not filled in: enter it on the quote' tip are untouched.", "TITLE: the dispatch's '!' is dropped and the changeset is minor, not major. Convention read from the repo, not chosen: the comparable enforce-or-remove sweep that deleted NINE inert published fields shipped under '### Minor Changes' (540e488 in CHANGELOG.md), user-visible removals like the knowledge Review Queue tab are minor, and the only major entry in the current changelog is a behaviour-breaking data-model change (the owner-field consolidation)." ], "files_changed": [ "src/objects/product.object.ts — the tax_rate field block and its '// Tax & billing' header (10 lines); the pricing field group survives on list_price/cost", "src/translations/en.ts:891 · es-ES.ts:1021 · ja-JP.ts:985 · zh-CN.ts:1051 — one crm_product row each. Enclosing object key PRINTED before every deletion; the four crm_quote_line_item rows (en:1308 es-ES:1506 ja-JP:1451 zh-CN:1436) are untouched and each pack went 2 tax_rate hits → 1", "content/docs/revenue/products.mdx · products.zh-Hans.mdx · products.zh-Hant.mdx — 4 sites each: frontmatter description, Pricing field-group summary row, the 'Default Tax Rate %' table row, the prose paragraph", "src/objects/quote_line_item.object.ts — the ':60' clause 'crm_product keeps tax_rate there too' only", "src/objects/opportunity_line_item.object.ts — the ':63' clause only (declared deviation)", "docs/STATUS.md — the validator transcript's field total (declared deviation)", ".changeset/remove-product-default-tax-rate.md — new, minor" ], "untouched_as_instructed": "src/data/revenue.seed.ts · docs/developers/api_reference.md · docs/feature-inventory.md · test/undeclared-key-probe.test.ts · test/line-item-conventions.test.ts · CHANGELOG.md · existing .changeset/*.md · AGENTS.md · CLAUDE.md · .claude/** · docs/adr/** · root skills/**. None was opened for writing.", "line_budget": "N/A — no skills/** file is touched, so the published-skill line ratchet does not apply. The repo's own token ratchet is the applicable budget and it is green with no ceiling raised: business semantics ~84,018 of ~85,000, authored total ~135,789 of ~140,000.", "tests": "REVERSE VERIFICATION, both legs run. BEFORE (unmodified 021db549): pnpm validate exit 0 'Data: 18 Objects 334 Fields'; pnpm test exit 0 'Test Files 159 passed (159) · Tests 3380 passed | 1 skipped (3381)'. AFTER: pnpm validate exit 0 'Data: 18 Objects 333 Fields' — exactly one field fewer, the control proving validate re-read the tree; pnpm test exit 0, the SAME 159 files and 3380 passed / 1 skipped, with the one documented figure updated. No test was adjusted and no assertion was weakened. ABLATION on the pin (documented above): mutation confirmed ON DISK before drawing any conclusion — '333 Fields' 1 → 0 occurrences and '334 Fields' 1 occurrence — then vitest test/docs-declared-versions.test.ts exit 1, 'Tests 1 failed | 18 passed'; the restore leg was 'git checkout HEAD -- ABSOLUTE_PATH' driven by a trap on EXIT INT TERM with REPO_ROOT resolved before the mutation, and it is proven by blob-hash equality with HEAD plus an empty git diff HEAD, not by a return code. No rebuild step is claimed or needed: the subject resolves through source (objectstack.config.ts loaded via tsx; the pin reads docs/STATUS.md off disk), there is no dist hop a stale artifact could hide in. Control-character self-scan over every changed file: clean.", "mcp_calls": "8 — issue_read get, issue_read get_comments, add_issue_comment (claim), list_pull_requests (checking for a competing claim on opportunity_line_item.object.ts), create_pull_request, pull_request_read (body read-back), add_issue_comment (this report), and the read-back of this comment. Every repo read was local git in the worktree.", "open_questions": [], "out_of_scope_findings": [ "NONE FILED this round, and none suppressed. The one drift I would otherwise have filed — docs/feature-inventory.md QUO-010/QUO-012 crediting crm_product with fields it does not declare — is already #1536 from the R32 run; I re-confirmed the file carries no tax_rate row of any kind, so this deletion neither touches it nor adds to it. The opportunity_line_item.object.ts:63 clause that R32 would have routed is fixed in this PR instead of filed, per the in-place-fix deviation above." ] }
Generated by Claude Code
Review — ACCEPT (director seat, summon #15,
session_01TezFG8ZMrNH6n5VTNpPpdH, 2026-09-05T15:4xZ) · PR #1588, headb9290b17Checked on GitHub against
references/review-checklist.md, not on the report:item reading PR shape draft ✓ · base main(021db549) ✓ · first lineFixes #1198✓ — correct: the ruling is executed in full; body scanned, no closing keyword near another open cardScope ( get_files, 12 files)the claimed surface exactly — product.object.ts(−10, the field block + its header), fourcrm_productlocale rows (the fourcrm_quote_line_itemrows untouched, verified in each patch), three doc faces × 4 sites,quote_line_item.object.ts:60clause, one changeset — plus two declared additions:docs/STATUS.md334→333 (the figuretest/docs-declared-versions.test.tsprotects; the dispatch authorised exactly this; ablation proven) andopportunity_line_item.object.ts:63(same comment-cross-reference class as the authorised:60clause, comment-only, no competing claim, declared in the PR body). Both meet the in-place-fix conditions; the claim's file surface was not amended before the push — recorded as a nit, not a REWORKRuling fidelity third option not implemented; only its one changeset sentence ✓; revenue.seed.ts,api_reference.md,feature-inventory.md, the line-item tests untouched ✓Docs zh-Hans / zh-Hant faces lose the same four sites as en; the:95quote-tax tip stays; no unrelated "fixes" on the pages ✓Changeset 'hotcrm': minorwith user-facing text; the!dropped on the measured precedent (540e488, nine inert fields under Minor Changes) — the seat accepts the repo's own convention ✓;Check ChangesetgreenEvidence real commands and outputs; validate 334→333 as the positive control; one pin red for the authorised reason and only that, proven by ablation with blob-hash restore; i18n gate 0; token ratchet lower, no ceiling moved ✓ CI on b9290b17at 15:4xZread in the same stroke as this comment — landing waits for every check to read successGoverned-surface path test: none of the twelve paths is governed ⇒ ordinary landing (ready → auto-merge squash).
Implemented-by:branchclaude/issue-1198-product-tax-rate-removal-2(mode:subagent) ·Reviewed-by:this session (same session — a PM review of a subagent's diff;Clause-②: no, no independent contract review owed).On MERGED: this card closes by the PR; #1199 stays
pm:queueawaiting its own maintainer ruling (comment 5552667389) and is now free of thesrc/translations/**fence.
Generated by Claude Code
- removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Sep 5, 2026
Split out of #1193 so the decision survives that card closing. #1193's deliverable was the object-aware scan that found this field; the field's own verdict is this issue.
The ruling of 2026-08-17 —「逐个 enforce-or-remove(推荐)」— authorised per-field adjudication for the ten rows enumerated in #1182. This field was not one of them — it was invisible to that card's scan — so the existing ruling does not reach it.
What the field is
crm_product.tax_rate("Default Tax Rate %",pricinggroup,defaultValue: 0). Read by nothing.pnpm scan:fields --sites crm_product.tax_rateon62560210:Four locale labels and nothing else — no view column, no form field, no formula, no hook, no flow. It is not on the product form either, so no admin can set it in the UI today; only the API can.
Enforce was the obvious answer, and it is wrong. Measured, not argued.
"Enforce" meant
createLineItemPriceFillstampingtax_ratefrom the product the way it already stampslist_price. Measured on the real engine (memory driver, shipped stack, seeds skipped) with a product attax_rate: 10,list_price: 1000, one line atquantity: 2:subtotaltotal_pricesubtotaltotal_priceThe quote total is 200 lower than the sum of its own line totals, because the two halves of this app model tax on different calibres and neither reads the other:
crm_quote_line_item.total_priceis a formula,quantity × unit_price × (1 − discount/100) × (1 + tax_rate/100)— a per-line percentage;quote_total_rolluprecomputes the quote from rawquantity/unit_price/discountand never reads the line'stax_rate; the quote's total issubtotal − discount_amount + tax + shipping_handling, wherecrm_quote.taxis a manual amount typed on the quote.So a product rate would produce a line item that disagrees with the quote it sits on, and a rep who also fills in
crm_quote.tax— which is the only field the rollup reads — taxes the customer twice.src/data/revenue.seed.tsalready refuses to seed a line rate for exactly this reason: "Seeding a rate here would put a second, contradictory tax number on the record."Two more things enforcement would have to solve, recorded so they are not rediscovered:
createLineItemPriceFillserves bothcrm_quote_line_itemandcrm_opportunity_line_item, and the latter declares notax_rateat all. A conditional is writable —ctx.objectis on the hook context, so the sandbox's body-only rule is not the obstacle — but it is a branch that exists only to keep one object out.crm_product.is_taxablewas removed in refactor(crm): enforce-or-remove every decorative field, one verdict each #1195, so "is this taxable" no longer exists as a concept. Only a rate does, and rate 0 has to carry the meaning "not taxed". That is fine arithmetically and it does mean a stamped 0 is indistinguishable from an unset one.What removal would cost, for whoever rules
crm_productfield list indocs/developers/api_reference.md;apiEnabledobject, so any value a customer has written to it through the REST API is lost. Nothing in this repo has ever written one — all 13 seeded catalog products leave it at the0default — so the demo org and every test are unaffected either way;crm_product.tax_rateis inert, and the consumer scan cannot see it — a same-named field on another object masks it #1193 changed it again to say plainly that nothing applies it. So the page is honest today under either outcome.The third option, which is a design change rather than an adjudication
Make the product rate drive the quote-level figure —
quote_total_rollupcomputingtaxas Σ(line subtotal × rate) instead of reading the manualcrm_quote.tax. That makescrm_product.tax_ratelive and the numbers consistent, but it changes the meaning of an existing published field on a different object (crm_quote.taxstops being an input), and the seeds carry explicit tax amounts that would become derived. That is a product decision, not an enforce-or-remove verdict, which is why it is listed rather than done.Recommendation: remove. Under the startup-focus principle it is a declared surface with no pull — no consumer, no UI affordance to set it, no seeded value, and its one plausible consumer is a second tax model this app deliberately does not have. Keeping it costs four locale rows and a docs sentence per release and invites exactly the misreading the page carried for months. But it is a published field, so the deletion is yours, not a sweep's.
Filed unassigned; no work started. Measurement reproducible from
62560210.