feat(opportunity): REQ-0006 qualification, the customer calendar, narrative and approval on status change - #1950
Conversation
…rative and approval on status change (REQ-0006) Eighteen new fields on `crm_opportunity`, two new derived `fieldGroups` sections, one new list view and one new approval flow — REQ-0006's B items, the `crm_opportunity` half of REQ-0002 steps 8 through 14. Qualification: will_bid, controllability, priority, deal_level, is_subcontracted and its note. The customer's own procurement calendar: customer_initiation_date, expected_tender_date, expected_signing_date and the two amounts — never folded into `close_date`, which is a single date and is OUR forecast close. Deal narrative: customer_background, project_background, risk_analysis, payment_terms. business_line sits BESIDE `type` and does not overload it. Signing entity and revenue-recognition type get no field at all, by REQ-0006 acceptance 5. Layout stays DERIVED: every field opts in with `group:` and no `record:details` section or `form.sections` entry is authored, so `pnpm validate` reports the data-entry fields as carrier-only. That reading is expected, not a defect, and the reason is recorded beside the block — the same verdict `crm_account`'s business-profile fields carry. The status-change gate is an `approval` node in a new record-change flow, and it SHIPS OFF: the switch is `status_change_approval_status`'s `defaultValue`, shipped `not_required`, so its start condition is false for every record that has ever existed and amount-tiered approval is bit-for-bit what it is today. Not the flow's `status` — `draft` still fires triggers and `obsolete` composes into a gate no install can turn on, both measured and recorded on the lead-conversion sibling. Because a record-change flow binds an AFTER hook, the rep writes a REQUEST (`requested_status`) and the approved decision writes `stage`, which is what acceptance 3's "the stage does not change until it is decided" requires. A transition gate in `opportunity.hook.ts` refuses a direct user move into a closed stage while the gate is armed; it reads the gate column input-first so the approving flow's own single-payload write passes. NOT LANDABLE AS IT STANDS: this exceeds the `src/sales` token ratchet — business semantics ~55,986 vs ~55,000 (over by ~986) and authored total ~101,395 vs ~100,000 (over by ~1,395). The tree is the measurement basis for that gap and awaits a maintainer ceiling ruling. Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PM 说明:这两道红是预期的,⛔ 本 PR 不由施工席修,也 ⛔ 不重跑 CI头 ①
|
|
| 文件 | 红 | 类别 |
|---|---|---|
test/i18n-references.test.ts |
6 | 四语言包未编写 |
test/automation-docs-coverage.test.ts |
5 | 新流程未进 content/docs/administration/automation.{mdx,zh-Hans,zh-Hant} |
test/docs-view-rosters.test.ts |
4 | 新视图 tender_this_quarter 未进文档名册 |
test/refusal-envelope.test.ts |
3 | 新拒绝点未进那份手维护的清扫计数 |
test/source-token-ratchet.test.ts |
1 | 阻塞物本身 |
test/lint-i18n-gate.test.ts |
1 | 同四语言包 |
test/runtime-coverage.test.ts |
1 | 新流程/hook 无运行时测试命名 |
test/metadata-references.test.ts |
1 | 同上游 |
test/docs-metadata-counts.test.ts |
1 | 文档所述计数与注册数不符 |
这改变了什么、没改变什么
⛔ 没改变:阻塞判断与缺口数字。上面 9 类里只有 source-token-ratchet 一条是计量面内的;其余八类动的全是 src/*/translations、content/docs/、test/ 与文档名册 —— 按裁决与按路径都不计入棘轮。⇒ business semantics 超 986、authored total 超 1,395,一个字不动。
改变了:裁决之后的收尾工作量。我上一条让人以为「裁完只剩四个语言包」,实际是四语言包 + 三处文档页 + 视图名册 + 拒绝清扫计数 + 运行时测试命名。这条我先前说少了。
责任在我,⛔ 不在施工席
施工席的报告明写「NOT MEASURED: full pnpm verify and CI, since hygiene:tokens fails inside it by construction」,并只声称「9 files / 314 tests 的定向子集里 2 条红」。它没有宣称全量绿。 是我把定向子集的结果当成了全量结论。
处置不变
- 本 PR 保持 draft,⛔ 不翻 ready、⛔ 不关闭。
- ⛔ 不重跑 CI —— 按构造不可能变绿。
- ⛔ 不为凑绿动任何守卫 —— 特别是
refusal-envelope.test.ts那份手维护计数:它故意是手维护的(自动跟随的计数发现不了一个悄悄不再被清扫的守卫),⛔ 不许改成自动。 - 裁决仍在 [decision] REQ-0006 needs +986 business-semantics and +1,395 authored-total tokens in src/sales — cutting scope does NOT avoid the question (measured), and a third route now exists #1951。裁下来后重新派发,上述八类一次做全。
Generated by Claude Code
`business semantics` 55,000 -> 59,000 and `authored total` 100,000 -> 107,000 in `scripts/check-source-token-ratchet.mjs`. Both are `anchor()` — the ruled `ceil(reading x 1.05 / 1000) x 1000` — of the readings PR #1950's tree prints (55,986 and 101,395), so the kind stays ANCHORED and the 5% working buffer is the whole of the raise. The maintainer ruling authorising it is quoted in the gate's own header and in the PR body, which is the condition the gate states for raising an anchored ceiling. The reading those two rows anchor from is not one `origin/main` prints, so the recorded anchoring run names the tree it was taken on (`refs/pull/1950/head` at 85e5dbd) — the format this header already uses for a run that cannot be taken on `main`. Nine of the twelve worked rows re-anchor onto that run; the three whose `anchor()` now lands above the ceiling they carry keep their 2026-09-16 row and are recorded as declined re-anchorings, which is what keeps the header's ledger self-consistent and `test/source-token-ratchet.test.ts` green. No other ceiling moves, and nothing under `src/` is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
Conflict: docs/STATUS.md, resolved by recomputing from the merged tree's own `pnpm validate` rather than by picking a side: main retired the demo_bootstrap flow (30 Flows) and this branch adds opportunity_status_change_approval, so the merged stack registers 18 Objects / 361 Fields / 31 Flows. Semantic conflict, resolved in this merge: main dropped `scale` from every currency field (#1968) and the 17.6.0 pin now refuses the key outright ("`scale` is not valid on a `currency` field — delete the key"). The two currency fields this branch adds, expected_tender_amount and expected_signing_amount, carried `scale: 2`; it is removed so the merged tree validates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…ion and narrative groups Since #1990 every Details section is a `{ group }` reference to one of crm_opportunity's fieldGroups, so a group the object declares renders on the record page only once the page names it. The two groups REQ-0006 adds are referenced right after `sales_process`, in the order the object declares them, and both keep `hideEmpty: false` on #1211's reasoning: every member is something the seller is expected to fill in, and no deal that predates REQ-0006 carries any of them, so the platform default would hide both sections on every existing deal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…ns once per request, and refuses with a declared code Three defects in the REQ-0006 gate as built, each found by reading it against its siblings and pinned by a new runtime file: - A rejected deal was ungated. The hook refused a direct close only while the verdict read `pending`, and the flow re-opened only from `pending`, so one rejection disarmed the gate on that deal for good: the rep could close it straight away with no approval. `rejected` now refuses too (as `lead_automation` refuses a conversion in both states) and a new request from `rejected` opens a fresh approval. - The start condition tested the current value, not the transition. The approval node's own `pending` stamp is an update of the deal while the request is open, so it re-fired the flow and the second run died on the plugin's DUPLICATE_REQUEST guard. The condition now requires the request to be new on the write (`previous.requested_status` differs, guarded fail-closed), the idiom `billing_handoff_closed_won` records. - The refusal carried `APPROVAL_REQUIRED`, which is no member of the platform ErrorCode enum, so the platform would have demoted it and derived the code from the status. It is now `RECORD_LOCKED` / 409, the declared `locked` class the lead gate uses; the hand-maintained refusal site count moves 20 -> 21. test/opportunity-status-change-approval-gate.test.ts: the shipped-OFF default, the start condition over every OFF and ARMED shape, both approval branches, the write-path refusal envelope, and the user-less run reaching the approval node (with the runAs counter-proof). Registered in runtime-coverage's RUNTIME_TEST_FILES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…r locale packs; the form offers the customer calendar The four `objects.pipeline.ts` packs (en, zh-CN, es-ES, ja-JP) gain every REQ-0006 field label and option label, `will_bid`'s help text, the `tender_this_quarter` view label and empty state, and the `qualification` and `narrative` section headings. They were left out of the measurement build on purpose until the ceiling ruling settled the field roster (#1951 ruling A); `pnpm lint:i18n-gate` now reads 0 `i18n/missing-*` issues. opportunity.view.ts: the `forecast` form section names the customer's procurement calendar (initiation, tender and signing dates and amounts). `tender_this_quarter` filters on `expected_tender_date`, and a filtered field that no form offers is a list view that can never match a deal a rep created through the UI (`test/metadata-references.test.ts`). The reason a `{ group: 'sales_process' }` reference cannot stand in is written beside the fields (AGENTS.md ladder rung 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…tive and status-change gate, for reps and admins
- content/docs/sales/opportunity-qualification{,.zh-Hans,.zh-Hant}.mdx:
the user-facing page REQ-0006's product response asks for — whether a
deal is worth pursuing, the customer's procurement calendar against our
close date, the deal narrative, the business line, and the optional
sign-off on a won/lost call (off by default; how a request, approval
and rejection play out; how an admin arms it). Business concepts, not a
field roster. Registered in the three sales meta files.
- content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdx: the
Opportunity Status Change Approval row, the header count 30 -> 31, and
a paragraph under Approvals; the ledger in
test/automation-docs-coverage.test.ts gains its two Chinese row labels.
- content/docs/sales/opportunities{,.zh-Hans,.zh-Hant}.mdx: the Tender This
Quarter row and section (ten saved views), the two new field groups and
the new fields in the field-group table, the customer calendar on the
form's Forecast tab, and the Details tab described as the object's
groups — it has been a list of `{ group }` references since #1990, so
"three sections holding seven fields" was already untrue and this
branch's two new sections would have made it more so. The zh-Hant name
is pinned in test/docs-view-rosters.test.ts.
- README.md: 30 -> 31 flows, the count the stack registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…line and request are editable
Measured on a fresh 17.6.0 boot of this branch: the record page's Details
tab draws the Qualification and Deal Narrative groups READ-ONLY (no inline
edit), and the Edit / New dialog is opportunity.view.ts's tabbed form,
which authors `sections` and therefore wins outright over the fieldGroups
derivation. So twelve of the eighteen REQ-0006 fields - the qualification
block, the narrative block, Business Line and Requested Status - rendered
but had no editing surface anywhere in the app, and with the gate armed a
rep could not raise a status-change request at all.
- `{ group: 'qualification' }` and `{ group: 'narrative' }` tabs: AGENTS.md
ladder rung 2, exactly as contact.view.ts renders REQ-0004's buying
centre. Nothing is enumerated; members, labels and icons come from the
object, so REQ-0006 acceptance 1 holds on the form from fieldGroups alone.
- `business_line` beside `type` on the Forecast tab, and `requested_status`
at the head of Win / Loss, visible only while the gate holds the deal
(pending or rejected): rung 3, each with its reason written beside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
… fields; correct ea40e43's reading ea40e43's message says the Details tab draws the REQ-0006 groups read-only. That reading was wrong: my probe missed the pencil. Re-measured on the same 17.6.0 boot, the pencil puts the whole Details tab into an edit mode with a Save bar. It edits every select, boolean, date, currency and textarea field, so the qualification block, the customer calendar, Business Line, Payment Terms and Requested Status can all be edited there (saved and read back through REST). It offers no editor for a MARKDOWN field, though: Customer Background, Project Background and Risk Analysis stay read-only, and so does the object's own `description`. The same boot also shows the tabbed Edit / New dialog renders no tab for a `{ group }` section. That holds for the two references ea40e43 added and for the contact form's REQ-0004 buying centre on `main`. A markdown field the form NAMES does get an editor (the account dialog's Description). So: - `business_line` and `requested_status` come out of the form again. Details edit mode covers them, and per-field enumeration was the escape hatch the ruling on this card refused. - `{ group: 'qualification' }` / `{ group: 'narrative' }` stay. They are the contract-correct rung-2 form, the same as the contact precedent, and they wait for the platform to render group sections in a tabbed form. They are not routed around (AGENTS.md: a platform defect is waited for). The note beside them now records what renders on 17.6.0 and what does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…sured rather than as read 92f90e2 said that without the transition term the approval node's own `pending` stamp re-fired the flow "and the second run died on the plugin's DUPLICATE_REQUEST guard". The re-fire is real; the effect was inferred from the plugin source and was wrong. Measured on a 17.6.0 boot with the gate armed and the term deleted (local only, restored): one re-entry per request, each caught by the engine's self-trigger guard, which warns that "the guard as authored does not exclude the flow's own write-back"; no DUPLICATE_REQUEST. With the term in place the same scenario logs no re-entry at all. The flow and test comments now say that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…tails tab can write them
Customer Background, Project Background and Risk Analysis move from
Field.markdown to Field.textarea, the same declaration `payment_terms`
already uses (label and group, nothing else). REQ-0006's product response
names "Field.markdown / Field.textarea per field".
Measured on the 17.6.0 console: the Details tab's edit mode is the only
surface the narrative group renders on, since the tabbed Edit dialog draws
no `{ group }` section. That edit mode gives every textarea an editor and
offers none for a markdown field, so the three were readable but could
never be written. The maintainer ruled the textarea option on #1950
(2026-10-03, 「改成 textarea(推荐)」). The object's own `description` stays
markdown; it is outside this card.
The note above the narrative block records the reason, and the form's
group-reference note no longer calls these fields unwritable. No label,
translation, docs page or changeset text named their type, so nothing else
changes. Token reading unchanged (both type names are eight characters).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
… raise, #1994, #1963 No conflicts. #1953 raises the src/sales ceilings to 59,000 / 107,000, the ruled answer to this branch's measurement. #1994 (tenant_admin profile) and #1963 (tsx bump, pnpm-lock.yaml) do not touch this branch's file surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
Fixes #1916
Clause-②: no. The change is app metadata on
crm_opportunityand adds one flow; it touches no published schema and no accept set.REQ-0006 for
crm_opportunity: the qualification block, the customer's procurement calendar, the deal narrative, a business line besidetype, and an approval gate on the won/lost call that ships switched off. One changeset names REQ-0006 (.changeset/opportunity-qualification-and-status-approval.md, minor).Every REQ-0006 acceptance line is delivered, with the evidence below. Every REQ-0006 field can be edited in the console, and
pnpm verifyis green on this branch at5a67d080(after #1953 raised thesrc/salesceilings to 59,000 / 107,000).What is on the branch
close_date.Field.textarea(see Editing surface).type.expected_tender_dateand never readsclose_date.opportunity-status-change-approval.flow.ts,requested_status,status_change_approval_status, and a transition gate inopportunity_lifecycle.defaultValue, shipped asnot_required. It is not the flow'sstatus.opportunity_account_capabilityhook (Track A · crm_account: registration identifier, a category that GATES capability, business profile, approval — REQ-0003 #1915), the two existingvalidations[], andopportunity-approval.flow.ts.Acceptance, line by line
fieldGroupsalone8333d7ee(served page sections equal to the compiled artifact and to source). The Details tab renders Classification, Campaigns, Sales Process, Qualification, Deal Narrative, Forecast & Metrics, Notes & Next Steps on a seeded deal and on a deal with only its required fields. The page references{ group }only; nothing is enumerated. On a fresh boot of033ba581, Details edit mode gave each of the three narrative fields an editor; typed values were saved, read back over REST, and shown after reload.close_dateexpected_tender_dateonly.409 RECORD_LOCKEDand the stage did not move. Requestingclosed_wonopened oneflow:opportunity_status_change_approvalrequest while the stage stayednegotiation. Approve moved the stage toclosed_wonwith the verdictapproved. Reject cleared the request and kept the stage; a direct close was still refused, and a re-request opened a fresh approval. Gate off, fresh boot of this branch: a direct close of a $5,000 deal succeeded, the verdict stayednot_required, and zero approval requests existed. A deal raised to $150,000 opened its usualflow:opportunity_approvalrequest, and the new column stayednot_required.opportunity-approval.flow.tsis unchanged.test/opportunity-status-change-approval-gate.test.ts: fired with no trigger user, the flow passesget_opportunityand stops only at the approval node (the harness ships no approval executor). WithrunAsstripped, it fails atget_opportunitywith the engine's[runAs]refusal.business_lineships Product, Professional Services, Consulting, Support & Maintenance and Other.pnpm verifygreen, and a changeset naming REQ-0006os-verify-lock: VERDICT command-exit 0on this branch at5a67d080(see Verification). The changeset.changeset/opportunity-qualification-and-status-approval.mdnames REQ-0006.Editing surface (measured, 17.6.0)
Field.markdowntoField.textarea(033ba581). That is the same declarationpayment_termsuses, and one of the two types REQ-0006 names. The maintainer ruled it on 2026-10-03: 「改成 textarea(推荐)」.descriptionstays markdown. It is outside this card and still has no editor in that mode.{ group }section. The same is true of the contact form's REQ-0004 buying centre onmain. A markdown field that a form names does get an editor (the account dialog's Description).{ group: 'qualification' }and{ group: 'narrative' }in the form. That is rung 2, like the contact precedent. They wait for the renderer, and they are not routed around.tender_this_quarterfilters onexpected_tender_date, andtest/metadata-references.test.tsrequires a filtered field to be authorable in a form.ea40e43aalso named Business Line and Requested Status on the form.3a907d2etook them out again: Details edit mode covers both, and the ruling on this card refused per-field enumeration.hideEmptyon the two new page sectionsBoth are
hideEmpty: false, for the reason #1211 gives in PR #1991. Every member is something the seller is expected to write, none of them is in the highlights strip, and no deal that predates REQ-0006 carries any of them. Without the key, both sections would vanish on exactly the deals where a seller needs to fill them in.Three defects fixed in the gate as built
Each was found by reading the gate against its siblings, pinned by the new runtime file, and ablated (every ablation went red in the direction decided beforehand, and each restore was proven to equal HEAD):
pendingrefused a direct close, so one rejection disarmed the gate for good.rejectednow refuses too (aslead_automationdoes), and the flow re-opens fromrejectedon a new request. Ablation: 1 of 28 red, the "refused by an approver" case.requested_statuson the write, guarded fail-closed, asbilling_handoff_closed_wondoes. Ablation: 2 of 28 red.92f90e28said the second run "died on DUPLICATE_REQUEST". That was inferred, not measured, and8333d7eecorrects the comment.APPROVAL_REQUIRED, which is not a platformErrorCode. It is nowRECORD_LOCKED/ 409, the declaredlockedclass. The hand-maintained refusal count goes from 20 to 21. Ablation: 4 red across the gate file andrefusal-envelope.Also in this round
main(25cd8d78).docs/STATUS.mdwas recomputed from the merged tree's ownpnpm validate: 18 Objects, 361 Fields, 31 Flows (mainretireddemo_bootstrap). One semantic conflict was resolved in the merge: the two new currency fields droppedscale, which 17.6.0 refuses outright (fix(objects): dropscalefrom every currency field — objectstack main refuses it #1968).lint:i18n-gatereads 0.sales/opportunity-qualification{,.zh-Hans,.zh-Hant}.mdx. The automation pages get the new row, a 31 count and an Approvals paragraph.opportunities{,…}.mdxget the view row and section, the new groups and fields, and the Details tab described as the object's groups (the old "three sections, seven fields" sentence has been untrue since feat(record-pages): Details tabs reference the objects' fieldGroups (lead, opportunity, case) #1990). README now says 31 flows.metadata-references, docs metadata counts andlint:i18n-gate. The refusal count was edited by hand (20 to 21) and stays hand-maintained, as that comment requires. No guard was skipped, disabled or loosened.Token ratchet (
src/sales)main25cd8d7 (before #1953)mainsince #1953History: this PR started as a measurement. On
85e5dbd, against087b7c5, it read 55,986 / 55,000 and 101,395 / 100,000, and #1951 ruling A answered it with 59,000 / 107,000 (PR #1953). No ceiling is raised here, no prose was compressed, and nothing was moved into an unmetered directory.Verification
5a67d080, after mergingmain2e3a8b4d, which carries chore(ratchet): raise the two src/sales token ceilings to their anchor() (59,000 / 107,000) #1953, feat(profiles): tenant_admin gains the org-scoped presentation authority (manage_org_presentation) #1994 and chore(deps-dev): bump tsx from 4.23.12 to 4.23.15 #1963 with no conflict):OS_VERIFY_LOCK_SLOT=hotcrm-issue-1916 bash scripts/pm/os-verify-lock.sh -c 'git rev-parse --short HEAD && pnpm verify'gaveos-verify-lock: VERDICT command-exit 0.Validation passed(18 Objects, 361 Fields) · lint1 warning(s), 17 suggestion(s)·0 i18n/missing-* issues·source hygiene clean·source token ratchet clean·Build complete·Test Files 175 passed (175),Tests 3735 passed | 1 skipped.033ba581with chore(ratchet): raise the two src/sales token ceilings to their anchor() (59,000 / 107,000) #1953's heade7287a65(a4b6743a) gave the same command-exit 0.mainhas 1 and 16. The 17th is the new approval node's "approvers may resolve empty" suggestion, the same class as its three siblings.Acceptance notes
{ group }form section, andrecord:detailsedit mode offers no editor for a markdown field — so group-referenced fields and markdown fields have no editing surface objectstack#21543:{ group }sections. This is visible today on the contact form's Buying Centre.descriptionstill does.quote_on_acceptedcloses the linked deal as won under the accepter's session. On an armed, pending deal that close would be refused by this gate.approved_date. The approving branch also stampsapproved_date, the column the amount-tiered flow writes. A later status approval therefore overwrites an earlier amount approval's date. This is left as built.opportunities.mdx's competitors bullet still calls the notes section "collapsible Description". That has been stale since feat(record-pages): Details tabs reference the objects' fieldGroups (lead, opportunity, case) #1990 and is outside this change.Body rewritten 2026-10-03T05:06Z, after #1953 landed and this branch verified green at
5a67d080. It receipts PR comments 5695540995, 5695672077 and 5695722225.Generated by Claude Code