fix(validations): guard null-aborting predicates, unify end_after_start, drop duplicate revenue rule - #571
Merged
Conversation
…rt, drop duplicate revenue rule Three families of validation declarations that `os validate` accepts and no test ever evaluated, so each failed silently rather than erroring. Unguarded CEL on crm_product: strict CEL aborts on `dyn<null> < int`, so an unguarded comparison skips the rule instead of failing it. `price_positive` and `cost_less_than_price` lacked the `!= null` guard that `quote_line_item.unit_price_positive` already models — and `cost` is absent on every seeded product, so that warning had never evaluated on a single row. end_after_start drift: campaign used `<`, accepting a campaign that ends the day it starts while its own message promised "must be after"; forecast used `<` with an "on or after" message contradicting its rule name; contract was already correct. All three now reject `end == start` and say "must be after". revenue_positive was declared twice with diverging wording — the validation said "must be positive", the hook threw "greater than or equal to 0", and both compared `< 0`, so the hook's wording was the accurate one. The duplicate declaration is dropped; the hook stays as the single, tested enforcement point. No seed data changes state under any of these: no seeded product has a negative list price or a cost, and no seeded campaign or forecast has end == start. New guards live in test/object-validation-predicates.test.ts — a repo-wide null-guard sweep over every ordering comparison in every object validation, the date-range twins pinned to one operator and one wording, and a single-enforcement-point check for annual_revenue. The sweep carries one documented exception, opportunity_line_item.unit_price_positive (the remaining half of item 3), with a companion test that fails if the entry goes stale. Refs #514 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019sj147MYMYNJEs26HDC6fw
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
July 31, 2026 09:45
3 tasks
yinlianghui
added a commit
to yinlianghui/hotcrm
that referenced
this pull request
Jul 31, 2026
main is red: test/object-validation-predicates.test.ts > 'keeps no stale entries in the known-unguarded list' fails with ["crm_opportunity_line_item.unit_price_positive"]. Nothing is broken — the guard fired exactly as designed. objectstack-ai#571 carved that rule out as a documented exemption because it belongs to a different object family, and paired the exemption with a staleness check so it could not rot. objectstack-ai#570, run in parallel, added the guard to that very rule. Both PRs were green on their own branch; the collision only exists on main, where the exemption is now stale. Emptying the map is the fix the check was written to force. verify: exit 0, 0 errors, 436 passed | 2 skipped, 24 files. Refs objectstack-ai#514. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Partial fix for #514, covering the three validation-declaration items only (3 — product half, 7, and 12). Every rule touched here is metadata that
os validateaccepts and that no test ever evaluated, so each failed silently: the rule did not error, it simply never fired.Each item was re-verified against current
main(5b88ad0) before being changed — #514 was written earlier and several of its entries have since been fixed by other PRs. All three below were still present.Type of Change
Related Issues
Refs #514 (items 3 — product half, 7, 12). This PR deliberately does not close the issue; the remaining items are out of scope.
Changes Made
product.object.ts). Strict CEL aborts ondyn<null> < int, so an unguarded comparison skips the rule rather than failing it.price_positive(record.list_price < 0) andcost_less_than_price(record.cost >= record.list_price) both lacked the!= nullguard thatquote_line_item.unit_price_positivealready models.costis absent on every seeded product, so that warning had never evaluated on a single row. Both operands now guarded.end_after_startoperator drift. The same rule had three spellings.crm_campaignused<, accepting a campaign that ends the day it starts while its own message promised "End Date must be after Start Date".crm_forecast.period_end_after_startalso used<, with an "on or after" message contradicting its own rule name.crm_contractwas already correct and is unchanged — it is the reference. All three now use<=in the violation predicate and say "must be after".revenue_positive.crm_accountdeclared a validation saying "Annual Revenue must be positive" whileaccount.hook.tsthrew "must be greater than or equal to 0" for the same condition — disagreeing about whether zero is allowed, though both compare< 0(it is). The duplicate declaration is removed and the hook remains the single enforcement point; it is also the tested one (test/hooks-runtime-sales.test.tsexecutes the handler, whereas the declaration was never evaluated by any test). The removal is documented in place, alongside the existing note aboutaccount_name_unique.Pbecame unused in that file and was dropped from the import.Seed-data impact
None. Adding a guard makes a previously-inert rule start firing, so this was checked per rule: no seeded product has a negative
list_priceor anycostat all, and no seeded campaign or forecast hasend == start. No seeded row changes validity.Scope note
Item 7's other half — the duplicate branch in
account.hook.ts— is intentionally left in place. Removing it would mean editing a hook and an existing test file, outside this PR's declaration-only scope, and the hook is the enforcement point being kept. Also observed but not changed, as it belongs to no assigned item:product.price_positive's message still says "must be positive" while comparing< 0, the same wording/semantics mismatch item 7 flags on account.Testing
pnpm test) — 398 passed, 2 skipped, 23 filespnpm lint)pnpm build)Full
pnpm verify(validate && typecheck && lint && hygiene && build && test) exits 0. The two expression warnings the build prints are pre-existing onmain(campaign_enrollmentflow) and unrelated.Tests were written first and confirmed failing on unmodified
main— 6 failures, one per real defect, withcrm_contract.end_after_startpassing from the start as the reference implementation.New file
test/object-validation-predicates.test.ts(deliberately not appended totest/metadata-references.test.ts, which is a high-conflict file):<,<=,>,>=) in every object validation carries its own!= nullguard;annual_revenue, asserting the declaration is gone and the hook check is still there.The sweep carries one documented exception —
opportunity_line_item.unit_price_positive, the remaining half of item 3, tracked separately — plus a companion test that fails if that entry ever goes stale, so the exception list can only shrink.Checklist
.changeset/validation-predicate-guards-and-operator-drift.mdAdditional Notes
The user-visible behaviour changes are narrow but real: a campaign or forecast whose end equals its start is now rejected where it was previously accepted, the two product rules begin firing on rows they used to skip, and a negative
annual_revenuenow reports the hook's message rather than the validation's.Generated by Claude Code