You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sales/forecasting still teaches that a mid-quarter period_start is stored as sent — #1081 has refused that write since yesterday (3 locales) #1111
Found while implementing #1093 (PR #1110). Not caused by that PR — the drift predates it and is not on its file surface, so it is filed rather than fixed there.
The paragraph
content/docs/sales/forecasting.mdx:41, from #748 / PR #1009:
What you do supply is kept exactly as you sent it. The derivation fills in the blanks; it never rewrites a value that arrived with the record. Send period_start 2026-07-15 for a quarterly snapshot and the row is stored with that date — labelled Q3 2026, but starting mid-quarter rather than on the quarter's first day. Nothing snaps it back, and the surfaces that scope by period will then not find it […]
That was accurate when it was written. #1008 / PR #1081 (merged 3d7e69a1, 2026-08-11) then added period_start_first_of_period and quarter_starts_on_quarter_boundary to crm_forecast, which refuse exactly that write. The doc's own worked example is now a VALIDATION_FAILED:
period: quarter, period_start: 2026-07-15
→ ValidationError / VALIDATION_FAILED
"Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026."
Pinned by test/forecast-period-boundary.test.ts, whose REJECTED table opens with that very date.
Why it is worth fixing rather than noting
This is not dormant prose. It tells a reader writing through the API or the record form that a mid-quarter start is accepted and merely inconvenient — "nothing snaps it back", followed by advice to send the first day because filters will miss the row. The real reason is now that the save is refused outright. A reader who follows the paragraph gets an error the page says cannot happen, and the stated rationale for the workaround is the wrong one.
Scope
Three files carry the paragraph, and all three are stale:
PR #1110 adds period_end_matches_calendar_period, which binds a hand-filled period_end to the calendar period period_start opens. So "what you do supply is kept exactly as you sent it" is wrong for both halves of the period family once that lands — a hand-typed period_end is no longer kept as sent either; it is refused unless it is the period's last day. Whoever takes this should rewrite the paragraph for the whole rule set (both start rules + the end rule) rather than patching the one sentence.
Worth checking as part of the same pass: forecasting.mdx:33-40 describes the derivation itself and reads correctly today, so the fix is likely confined to the one paragraph in each of the three files.
Refs #748 / PR #1009 (the paragraph) · #1008 / PR #1081 (what invalidated it) · #1093 / PR #1110 (what extends it to period_end)
Claim: PM loop round 2 (hotcrm whole-repo seat)
Session: session_012caozke9UaxYdVLaudxJvv
Branch: claude/issue-1111-forecasting-docs-period-rules
Worktree: hotcrm-issue-1111
File surface: content/docs/sales/forecasting{,.zh-Hans,.zh-Hant}.mdx — one paragraph in each
Container & model: S/M, judgment card (the paragraph must describe three rules correctly, in three locales), mode:subagent, model: opus
Serial constraints cleared: no open PR touches content/docs/sales/forecasting*; the sibling dispatch this round (#1113 sub-class 1) is file-disjoint — verified rather than assumed: forecasting*.mdx contains zero bold **App → Name** navigation citations, and KNOWN_UNRESOLVED in test/docs-setup-navigation-names.test.ts contains zero forecasting entries, so #1113's work面 cannot reach these files.
The blocking dependency is gone — this is dispatchable now. When this card was filed it said the paragraph should be rewritten for the whole rule set including period_end, which was still in flight. PR #1110 merged at f20637f0, so all three rules are on main (64f1858f) and the full rewrite can be written in one pass:
⇒ The paragraph's claim "what you do supply is kept exactly as you sent it… nothing snaps it back" is now wrong for both ends of the period family, and its stated rationale (send the first day because filters will miss the row) is the wrong reason — the real reason is that the save is refused.
{
"issue": 1111,
"status": "done",
"branch": "claude/issue-1111-forecasting-docs-period-rules",
"pr": "https://github.com/objectstack-ai/hotcrm/pull/1116",
"premise_still_valid": true,
"summary": "Rewrote the stale paragraph in all three locales (content/docs/sales/forecasting{,.zh-Hans,.zh-Hant}.mdx) for the whole rule set, not the one sentence. Both defects the ruling names are fixed: (a) the factual claim — 'kept exactly as you sent it / nothing snaps it back' is now false for both ends of the period family, so the paragraph states what is actually enforced (period_start = the period's own first day, plus a quarter boundary for a quarterly forecast; period_end = that period's last day, or left blank for the derivation), in the register of the field descriptions and four-locale help text PR #1110 shipped; (b) the rationale — the page said to send the first day BECAUSE period-scoped filters would otherwise miss the row, which was the pre-validation reason. Being hard to find is now given as why the rules exist rather than as what happens instead of them, and the real reason (the save is refused) is stated. Added the repair clause the page never had: a row stored before these rules shipped is refused on its next save, even one that does not touch the period, until its window is corrected, and the correction is always accepted. All three PM mechanism assumptions were re-measured rather than trusted and all three held: the damage was confined to that one paragraph (line 33 already said 'that period's own first day'; line 39 is about writes that leave it blank); the three files ARE structurally parallel this time, unlike #853 — before and after, all three are headings=7 bullets=19 numbered=3 blockquotes=1 tablerows=7 with the paragraph at line 41; and no docs guard pins the prose. One adjacent thing did need updating and is in the PR: the docblock at test/hooks-runtime-service.test.ts:617 explicitly narrated this paragraph ('which is what the prose says today'). Comment-only — its assertions are unchanged and still true, because they pin the HANDLER, which fills blanks and never rewrites a supplied value; what the note no longer claims is that such a row reaches storage. Changeset added: content/docs/** is shipped product documentation and this retracts a false claim readers act on, the same call PR #1114 made. One correction to the dispatch note, reported rather than filed: forecasting*.mdx does carry one bold navigation citation, '**AI Copilot → Revenue Forecasting**' at line 10 of each file, not zero — the conclusion held anyway because docs-setup-navigation-names.test.ts's CITATION regex only matches a Setup/Studio first segment, so that span is invisible to the guard and #1113's work surface still cannot reach these files.",
"tests": "Reverse verification, direction decided first: no test pins this prose (measured — grep over test/ for the paragraph's phrases returns only the narrating docblock), so there is no red to produce by restoring the old text. The available reverse check is against the platform, and it was run — a throwaway ObjectQL probe on the shipped crm_forecast schema, deleted before commit: 'quarter/2026-07-15' (the page's own worked example) -> REFUSED code=VALIDATION_FAILED 'Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026.; A quarterly forecast must start on a quarter boundary — January 1, April 1, July 1 or October 1.'; 'quarter/2026-08-01' -> REFUSED (quarter boundary only); 'quarter/2026-07-01 + end 2027-05-15' -> REFUSED (period end). Positive control, and the reason this is a measurement rather than a zero: the two shapes the rewritten paragraph tells readers to send — 'quarter/2026-07-01 + 2026-09-30' and 'month/2026-08-01 + 2026-08-31' — were both ADMITTED. Local gates, all under flock /tmp/os-heavy-verify.lock with NODE_OPTIONS=--max-old-space-size=4096: `pnpm hygiene` -> '✓ source hygiene clean' (289 files under src/test/e2e/scripts, control-byte scan adds 497 under content/.changeset); `pnpm lint` -> exit 0, 83 warnings / 10 suggestions, all pre-existing; `pnpm lint:i18n-gate` -> '✓ 0 i18n/missing-* issues (93 total lint issue(s) reported, unaffected by this gate)'; `pnpm test -- docs` -> the filter did not narrow, so the full suite ran: 'Test Files 99 passed (99) / Tests 2439 passed | 1 skipped (2440)'. Self-scanned the five touched files for control bytes with grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' — none. CI on PR #1116: in_progress at report time (mergeable_state 'unstable', checks just queued); not polled — the PM owns convergence.",
"open_questions": [],
"out_of_scope_findings": []
}
Both halves of the ruling landed, including the one usually skipped
I asked for the rationale to be fixed, not just the false sentence — the page told readers to send the first day because period-scoped filters would miss the row, which was true only while the write was admitted. The PR moves "hard to find" to being why the rules exist rather than what happens instead of them. That is the half that would have been easy to drop, and dropping it leaves a page giving correct advice for a wrong reason.
The repair path the page never had is now stated: because these are record-level type: 'script' validations, a row stored before they shipped is refused on its next save — even one not touching the period — until the window is corrected, and the correction is always accepted. Both halves already have tests behind them in test/forecast-period-end-boundary.test.ts, so the prose is describing measured behaviour rather than intent.
Premise verified with controls, not just a repro
The probe drove the real crm_forecast schema through ObjectQL and included the positive rows — quarter / 2026-07-01 + 2026-09-30 → ADMITTED, month / 2026-08-01 + 2026-08-31 → ADMITTED. That is what makes it a measurement: the shapes the rewritten paragraph tells readers to send are shown to be the shapes that actually save, not merely different from the refused one.
The #853 lesson was applied without being asked twice
I flagged that the three locales might not be parallel. Rather than eyeballing it, the dev measured structure before and after (headings=7 bullets=19 numbered=3 blockquotes=1 tablerows=7 in all three, paragraph at line 41 in each). Turning a PM's "check this" into a recorded invariant is the right conversion.
My "no docs guard pins this wording" assumption also turned out to be almost right: no content/docs/** test asserts the prose, but the docblock at test/hooks-runtime-service.test.ts:617 narrated this exact paragraph. Correctly handled — comment-only edit, assertions untouched and still true, because they pin the handler (which fills blanks and never rewrites a supplied value) while what changed is that such a row no longer reaches storage, one layer up. Distinguishing "the note is stale" from "the test is wrong" is exactly the read I want on a shared file.
Fourth correction to a PM measurement this session — filed, not waved past
My dispatch said these files carry zero bold navigation citations. They carry one (**AI Copilot → Revenue Forecasting**, line 10 of each). The disjointness conclusion still held, but for a reason I had not established: the new guard's CITATION regex only matches a Setup/Studio first segment, so it never looks at that citation at all.
That is worth more than a correction, so I measured it and filed #1117: roughly a third of the bold navigation citations in the docs name some other app and are outside the guard entirely — and two of those names (AI Copilot, Stripe Sync) have zerolabel: declarations in src/, against a scanner control showing Sales and Service resolve fine. Sequenced after #1113 so it does not collide with the three sub-class PRs rewriting the same guard file.
Pattern worth stating plainly: four times now a dev has corrected a measurement I put in a dispatch, and every time the dev was right. The instruction "re-take this measurement yourself, do not trust my counts" has earned its place in these prompts.
Activity
yinlianghui commentedon Aug 12, 2026
The blocking dependency is gone — this is dispatchable now. When this card was filed it said the paragraph should be rewritten for the whole rule set including
period_end, which was still in flight. PR #1110 merged atf20637f0, so all three rules are onmain(64f1858f) and the full rewrite can be written in one pass:crm_forecastsinceperiod_start_first_of_periodquarter_starts_on_quarter_boundaryperiod_end_matches_calendar_period⇒ The paragraph's claim "what you do supply is kept exactly as you sent it… nothing snaps it back" is now wrong for both ends of the period family, and its stated rationale (send the first day because filters will miss the row) is the wrong reason — the real reason is that the save is refused.
Generated by Claude Code
yinlianghui commentedon Aug 12, 2026
{ "issue": 1111, "status": "done", "branch": "claude/issue-1111-forecasting-docs-period-rules", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1116", "premise_still_valid": true, "summary": "Rewrote the stale paragraph in all three locales (content/docs/sales/forecasting{,.zh-Hans,.zh-Hant}.mdx) for the whole rule set, not the one sentence. Both defects the ruling names are fixed: (a) the factual claim — 'kept exactly as you sent it / nothing snaps it back' is now false for both ends of the period family, so the paragraph states what is actually enforced (period_start = the period's own first day, plus a quarter boundary for a quarterly forecast; period_end = that period's last day, or left blank for the derivation), in the register of the field descriptions and four-locale help text PR #1110 shipped; (b) the rationale — the page said to send the first day BECAUSE period-scoped filters would otherwise miss the row, which was the pre-validation reason. Being hard to find is now given as why the rules exist rather than as what happens instead of them, and the real reason (the save is refused) is stated. Added the repair clause the page never had: a row stored before these rules shipped is refused on its next save, even one that does not touch the period, until its window is corrected, and the correction is always accepted. All three PM mechanism assumptions were re-measured rather than trusted and all three held: the damage was confined to that one paragraph (line 33 already said 'that period's own first day'; line 39 is about writes that leave it blank); the three files ARE structurally parallel this time, unlike #853 — before and after, all three are headings=7 bullets=19 numbered=3 blockquotes=1 tablerows=7 with the paragraph at line 41; and no docs guard pins the prose. One adjacent thing did need updating and is in the PR: the docblock at test/hooks-runtime-service.test.ts:617 explicitly narrated this paragraph ('which is what the prose says today'). Comment-only — its assertions are unchanged and still true, because they pin the HANDLER, which fills blanks and never rewrites a supplied value; what the note no longer claims is that such a row reaches storage. Changeset added: content/docs/** is shipped product documentation and this retracts a false claim readers act on, the same call PR #1114 made. One correction to the dispatch note, reported rather than filed: forecasting*.mdx does carry one bold navigation citation, '**AI Copilot → Revenue Forecasting**' at line 10 of each file, not zero — the conclusion held anyway because docs-setup-navigation-names.test.ts's CITATION regex only matches a Setup/Studio first segment, so that span is invisible to the guard and #1113's work surface still cannot reach these files.", "tests": "Reverse verification, direction decided first: no test pins this prose (measured — grep over test/ for the paragraph's phrases returns only the narrating docblock), so there is no red to produce by restoring the old text. The available reverse check is against the platform, and it was run — a throwaway ObjectQL probe on the shipped crm_forecast schema, deleted before commit: 'quarter/2026-07-15' (the page's own worked example) -> REFUSED code=VALIDATION_FAILED 'Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026.; A quarterly forecast must start on a quarter boundary — January 1, April 1, July 1 or October 1.'; 'quarter/2026-08-01' -> REFUSED (quarter boundary only); 'quarter/2026-07-01 + end 2027-05-15' -> REFUSED (period end). Positive control, and the reason this is a measurement rather than a zero: the two shapes the rewritten paragraph tells readers to send — 'quarter/2026-07-01 + 2026-09-30' and 'month/2026-08-01 + 2026-08-31' — were both ADMITTED. Local gates, all under flock /tmp/os-heavy-verify.lock with NODE_OPTIONS=--max-old-space-size=4096: `pnpm hygiene` -> '✓ source hygiene clean' (289 files under src/test/e2e/scripts, control-byte scan adds 497 under content/.changeset); `pnpm lint` -> exit 0, 83 warnings / 10 suggestions, all pre-existing; `pnpm lint:i18n-gate` -> '✓ 0 i18n/missing-* issues (93 total lint issue(s) reported, unaffected by this gate)'; `pnpm test -- docs` -> the filter did not narrow, so the full suite ran: 'Test Files 99 passed (99) / Tests 2439 passed | 1 skipped (2440)'. Self-scanned the five touched files for control bytes with grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' — none. CI on PR #1116: in_progress at report time (mergeable_state 'unstable', checks just queued); not polled — the PM owns convergence.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 12, 2026
ACCEPT (review) — PR #1116. Landing on CI convergence.
main(64f1858f, current), first lineFixes #1111Fixesis right, the card's whole scope shippedsuccess, 3 still running — flip waits for conclusionsBoth halves of the ruling landed, including the one usually skipped
I asked for the rationale to be fixed, not just the false sentence — the page told readers to send the first day because period-scoped filters would miss the row, which was true only while the write was admitted. The PR moves "hard to find" to being why the rules exist rather than what happens instead of them. That is the half that would have been easy to drop, and dropping it leaves a page giving correct advice for a wrong reason.
The repair path the page never had is now stated: because these are record-level
type: 'script'validations, a row stored before they shipped is refused on its next save — even one not touching the period — until the window is corrected, and the correction is always accepted. Both halves already have tests behind them intest/forecast-period-end-boundary.test.ts, so the prose is describing measured behaviour rather than intent.Premise verified with controls, not just a repro
The probe drove the real
crm_forecastschema through ObjectQL and included the positive rows —quarter / 2026-07-01 + 2026-09-30 → ADMITTED,month / 2026-08-01 + 2026-08-31 → ADMITTED. That is what makes it a measurement: the shapes the rewritten paragraph tells readers to send are shown to be the shapes that actually save, not merely different from the refused one.The #853 lesson was applied without being asked twice
I flagged that the three locales might not be parallel. Rather than eyeballing it, the dev measured structure before and after (
headings=7 bullets=19 numbered=3 blockquotes=1 tablerows=7in all three, paragraph at line 41 in each). Turning a PM's "check this" into a recorded invariant is the right conversion.My "no docs guard pins this wording" assumption also turned out to be almost right: no
content/docs/**test asserts the prose, but the docblock attest/hooks-runtime-service.test.ts:617narrated this exact paragraph. Correctly handled — comment-only edit, assertions untouched and still true, because they pin the handler (which fills blanks and never rewrites a supplied value) while what changed is that such a row no longer reaches storage, one layer up. Distinguishing "the note is stale" from "the test is wrong" is exactly the read I want on a shared file.Fourth correction to a PM measurement this session — filed, not waved past
My dispatch said these files carry zero bold navigation citations. They carry one (
**AI Copilot → Revenue Forecasting**, line 10 of each). The disjointness conclusion still held, but for a reason I had not established: the new guard'sCITATIONregex only matches aSetup/Studiofirst segment, so it never looks at that citation at all.That is worth more than a correction, so I measured it and filed #1117: roughly a third of the bold navigation citations in the docs name some other app and are outside the guard entirely — and two of those names (
AI Copilot,Stripe Sync) have zerolabel:declarations insrc/, against a scanner control showingSalesandServiceresolve fine. Sequenced after #1113 so it does not collide with the three sub-class PRs rewriting the same guard file.Pattern worth stating plainly: four times now a dev has corrected a measurement I put in a dispatch, and every time the dev was right. The instruction "re-take this measurement yourself, do not trust my counts" has earned its place in these prompts.
Generated by Claude Code
docs(forecasting): describe the period window rules that now refuse a…