Repository navigation
fix(forms): a quote line no longer requires a Sales Price, and a mid-period Period Start is refused naming the start alone (#2060 items 7b, 7c) - #2196
Merged
objectstack-fleet[bot] merged 4 commits intoOct 20, 2026
Conversation
…fills (#2060 item 7b) crm_quote_line_item.unit_price was `required`, so the New Quote Line Item form refused "Please check the highlighted fields: Sales Price" and sent nothing, while the shared price fill (quote_line_item_price_fill) stores the product's list price on every insert that leaves it blank (measured on 18.0.0-next.5: REST insert without a price, 201 with unit_price 75000 = list_price). This is item 7's A-21 change on the twin line object: `required` is dropped, `storage.notNull` keeps the column whole, and the hook stays the producer, so both line objects declare the price alike. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…rt alone (#2060 item 7c) On a start off its calendar boundary, period_end_matches_calendar_period judged a window that is no calendar period. Measured on 18.0.0-next.5: New Forecast, Month, Period Start 2026-10-15 and no Period End answered 400 with "Period Start must be the first day …; Period End must be the last day …", because the hook derives an end and the end rule judged it. The same rule refused a typed 2026-10-31 and an edit that moved only the start, and passed a typed 2026-11-14. Each branch of the end rule now also requires the start its period opens on, in the start rules' own patterns, so the rule stands down exactly when a start rule refuses the row. Of the two remedies the card names, this one was measured to cover every shape above; stopping the hook's derivation cleared only the blank-end insert. The hook is unchanged. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…fusal (#2060 items 7b, 7c) 7b: crm_quote_line_item.unit_price joins the NOT NULL server-filled declaration pin, and the Sales Price block runs on both line objects: a line with no price lands at list, a negotiated price is kept, the dry-run preview no longer reports the price missing, and a clear on update is refused by the column. 7c: every mid-period start the boundary suite refuses now names no Period End, and a new block pins the window rule standing down while a start rule fires (the blank-end insert, a typed calendar end, an edit that moves only the start) with the control that it judges the end again once the start is on its boundary. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
… names (#2060 items 7b, 7c) forecasting (en, zh-Hans, zh-Hant) gains one sentence on the period_end bullet: while period_start is itself off its boundary, the refusal names period_start alone, and the end is judged once the start opens a real period. The quotes page already says a line's unit price defaults to the product's list price, which the form now honours, so it is unchanged. The changeset describes both items for the release notes. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Author
席位核对ACCEPT (#2060 items 7b, 7c) · PR #2196 at
Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing Generated by Claude Code |
This was referenced Oct 20, 2026
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.
Part of #2060 (items 7b, 7c)
Clause-②: no
The two residues of item 7 that comment
6281263541folded onto the card, under claim6286510541. #2060 remains open: this PR delivers items 7b and 7c only, and the card's other open items are untouched.7b. A quote line no longer requires a Sales Price the server fills
crm_quote_line_item.unit_pricedropsrequiredand keepsstorage.notNull.quote_line_item_price_fill(the shared_line-item-price-fill.ts) stays the producer. This is PR #2185's A-21 change on the twin line object, so the two line objects declare the price the same way, as the shared fill's header asks.Measured as the rep
na.rep@objectos.aion the rep's own draft quote, with the product Implementation Services (list price 75,000). Each column is a dev server booted from a fresh database withdemo:staff. Console readings are headless Chromium at 1440x900.maine6d5d3eunit_priceanswers 201, toast "Quote Line Item created successfully", and the list shows Sales Price 75,000.00POST /api/v1/data/crm_quote_line_itemwithout a priceunit_price75000 =list_pricePATCHwithunit_price: nullVALIDATION_FAILED"Sales Price is required and cannot be cleared"VALIDATION_FAILED"unit_price is required", stored price unchanged. The NOT NULL refusal's wording is objectstack#24715's, as on deal linesvalidate, then REST import withdryRun: true7c. A mid-period Period Start is refused naming Period Start alone
Cause (mechanism assumption 2, confirmed):
forecast_derive_periodderives an end from an off-boundary start, andperiod_end_matches_calendar_periodthen judges it. Its arithmetic, start + one period − 1 day, matches the calendar period only for a start the two start rules admit.Console, before and after. The sales manager opens New Forecast, keeps Period Month, enters Period Start 2026-10-15 and leaves Period End blank. The form sends
{period: month, source: scheduled, period_start: 2026-10-15}.main: the toast reads "Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026.; Period End must be the last day of the period — …". The answer is 400 with two rule violations.The two remedies the card names, both measured. Each is a scratch edit made through
ablation-replace.mjs, booted on the real engine as the manager (hooks.run), and restored to a blob identical to HEAD. In the table, START, QTR and END stand for the start, quarter-boundary and window refusals.main{period: quarter}only (the nightly sweep's shape)On
mainthe end rule refused a correct calendar end (2026-10-31) and admitted a non-calendar one (2026-11-14). Its verdict on an off-boundary start meant nothing.B is shipped. Each branch of the end rule now also requires the start that its period opens on, written in the start rules' own patterns. So the rule stands down exactly when a start rule refuses the row. The hook is unchanged. The choice, on the four axes:
Real business need (measured). Three writers set a forecast start. The console form (a sales manager) and the
revenue_forecastingskill are two of them. The third,forecast_snapshot, sendsperiodonly, and neither remedy touches it (last row). Both remedies fix the card's row. A leaves three shapes a person reaches in the form, and the console names Period End on each of them:B names the start alone on every one of them.
Long-term soundness. The object already documents the three rules as "one set enforcing one invariant", with the end rule's claim true "ONLY because the two rules above pin the start". B writes that premise into the predicate. A would teach the hook what a boundary is, a second place that knows it. The object's own note argues against that ("These rules are the ONE enforcement point — deliberately not a throw in
forecast.hook.tsas well"), and so does the end-boundary suite's scope pin.Making AI errors harder. B is not tolerance. The set of refused rows is unchanged (in the table, every row refused on
mainis refused under B, and every admitted row is admitted). Only the second message goes, and the refusal names the one field to fix. Under A, an agent reading the hook would find boundary logic that reads like enforcement.Startup focus. Both are one-expression changes. Neither adds a field, a rule or a gate. B touches one predicate and no hook.
REST on this branch, as the manager:
Docs
content/docs/sales/forecasting(en, zh-Hans, zh-Hant) gains one sentence on theperiod_endbullet: whileperiod_startis off its boundary, the refusal namesperiod_startalone.content/docs/sales/quotesalready says a line's unit price defaults to the product's list price. The form now honours that, so the page is unchanged.content/docs/was grepped uncapped in all three locales for the price and the period rules.Tests
test/form-required-server-defaults.test.ts:crm_quote_line_item.unit_pricejoins the NOT NULL server-filled declaration pin.test/forecast-period-boundary.test.ts: every mid-period start the suite refuses now asserts that no Period End message comes with it.test/forecast-period-end-boundary.test.ts, new block "the window rule stands down while a start rule fires":Full suite on 8e2d307, 3 lock calls of
vitest run --shard=k/3 --maxWorkers=2, eachVERDICT command-exit 0. Together that is 202 files, equal to the count ofgit ls-filestest files:Ablations on 131cf84, one per changed behaviour, each through the lock with
objectstack/scripts/ablation-replace.mjs. The anchor hit once and the blob moved; the restore read blob == HEAD with an emptygit diff HEAD.required: trueback on the quote line'sunit_price.test/form-required-server-defaults.test.tsreads 3 failed | 21 passed: the declaration pin, the preview and the clear refusal, all on the quote leg. The first attempt was a no-op: its anchor was a prefix of its replacement, so the tool refused it (anchor count 1 to 1) and restored before any test ran. The second attempt is the one reported here.Gates on 8e2d307
The
verifychain ran in one lock hold,VERDICT command-exit 0. Each exit code was captured before any pipe.pnpm validatepnpm typecheckpnpm lintpnpm lint:i18n-gatei18n/missing-*issuespnpm hygienepnpm hygiene:tokenspnpm buildpnpm testThe control-byte self-scan over the 9 changed files exits 1 (none found).
Acceptance notes
period(the picklist) andperiod_label(the free-text label) both read "Period":src/sales/translations/en/objects.pipeline.tsgivesperiod_labelthe label 'Period', while the field declares "Period Label". This is outside this PR's surface and is reported to the seat.demo:staffexits 1 on a fresh database. It reports "crm_task routed no rows at all" and the same forcrm_event. This happened identically onmainand on this branch, and is unrelated to this change. No one carries it.Generated by Claude Code