Repository navigation
feat(account): registration identifier, business profile, and account approval (REQ-0003) - #1931
os-elon-musk wants to merge 1 commit into
Conversation
… approval (REQ-0003) Adds the `crm_account` half of REQ-0002's steps 1, 3, 4 and 5, per REQ-0003: - `registration_number` — the counterparty's registered identity, with a country-neutral name and label (a field named after one country's registry could not be renamed by any install), searchable and indexed so the value is findable from global search and the lookup picker. - `commercial_capability` — the category that is meant to gate capability. Ships as metadata only; see below and the field's own docblock. - `incumbent_vendor`, `annual_purchasing_budget`, `payment_cycle` — the business-profile block, in a new `business_profile` fieldGroup so the layout stays derived (no `record:details` section is authored). - `approval_status` + `account-approval.flow.ts` — an `approval` node inside a `record_change` flow, the construct `opportunity-approval.flow.ts` already uses. Ships armed, because REQ-0003 acceptance 4 asks for that in as many words; the switch is the field's `defaultValue`, never the flow's `status`. All four locale packs, the automation-page flow table in all three doc locales, and the STATUS.md / README counts move with it.⚠️ THE CAPABILITY GATE IS NOT IN THIS COMMIT, and cannot be authored as REQ-0003 line 128 specifies it. A `validations[]` predicate on `crm_opportunity` is evaluated against plain data — the opportunity's own stored columns — so it cannot read its account's category: relation traversal throws `No such key`, and `os.lookup` has no overload because `buildScope()` in `@objectstack/formula` never binds `ctx.api`. An unevaluable predicate rejects the write, so authoring it literally would refuse every opportunity write. Measurements are in the field's docblock and the PR body.⚠️ `pnpm verify` is RED on the source token ratchet: business semantics is ~53,837 against a ~53,000 ceiling, over by ~837. Raising that ceiling needs a maintainer ruling (hotcrm#1928) and is deliberately not attempted here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
PM 复核完毕:裁定 ESCALATE。⛔ 这个红不由本 PR 修,本 PR 保持 draft、⛔ 不入队。 红的是什么
上游全绿( 为什么不在这里修抬天花板 ⛔ 不是本 PR 能做的。
那条裁决在 #1928,已带实测缺口进维护者决策箱。 其余三条出路 PM 逐条排除过:
⛔ 不重跑这个失败是确定性的,PM 在本地逐字复现过。重跑只是再浪费一轮 CI。 本 PR 还缺什么(与上面那条无关的第二件)验收第 2 条(能力闸)没有可写形式:REQ-0003 第 128 行指定 ⇒ 所以本 PR 正文用的是 两件待裁都在维护者那里:#1928(天花板)与 #1915(闸门构造,且不论选哪条都要改 REQ-0003 的 Product response)。 Generated by Claude Code |
Superseded by #1948 — closingThis PR's gate construct was overturned by the maintainer ruling of 2026-09-16 (recorded on #1915), quoted verbatim:
Two things changed underneath this branch, so it is re-cut rather than continued:
The replacement is #1948, cut fresh from
Generated by Claude Code |
Part of #1915 — REQ-0003. This PR does not complete that card and must not close it: the capability gate (acceptance 2) is absent, and
pnpm verifyis red on the source token ratchet. Both are decisions above this PR's floor, detailed below. #1915 remains open.⛔ Blocker 1 — the source token ratchet has no room, by ~837 tokens
Measured with
pnpm hygiene:tokens, three readings on this branch:391990e(base)account.object.tsfieldsaccount-approval.flow.ts+ registrationThe card's whole content costs ~1,458 tokens against ~621 of headroom. The approval flow alone is ~886, and its nearest sibling
lead-conversion-approval.flow.tsmeasures ~926 — so acceptance 4 could not have fitted in the headroom under any authoring.⛔ The ceiling is not raised here.
scripts/check-source-token-ratchet.mjsrequires a maintainer ruling quoted in the raising PR's body, and that decision card (#1928) is already in the box. ⛔ Nor is unrelated prose compressed to make room — comments are stripped before the count, so that would not have helped anyway; the gate says so itself.This is the only reason
pnpm verifyis red. Everything upstream ofhygiene:tokensis green, and the test suite is 3,530 passed / 3 failed, where all 3 failures are the ceiling and its two README-figure derivatives:⛔ Blocker 2 — the capability gate cannot be authored as REQ-0003 specifies it
REQ-0003 line 128 pins the construct: "Authored as a
validations[]entry oncrm_opportunityreading its account's category". A validation predicate oncrm_opportunitycannot read its account's category. Measured on the pinned packages, not inferred:checkPredicate()in@objectstack/objectqlevaluates the rule asExpressionEngine.evaluate(expr, { record: merged, previous }). Both are plain data objects — the opportunity's own stored columns. Noapiis passed.record.crm_account.type→runtime: No such key: type.os.lookup("crm_account", …)does not work either, and this is the trap.EvalContextin@objectstack/formuladocuments an optionalapi.lookup, butbuildScope()builds theosobject from onlyctx.user/ctx.org/ctx.envand never bindsctx.api— so the function has no overload even when a caller passes one:runtime: found no matching overload for 'dyn.lookup(string, dyn)'.The platform's own liveness diagnostic reaches the same verdict from the other side, unprompted:
What is not the problem — the card's stated worry is resolved
#1915 asked whether the #1914 lesson applies (a validation sees
{...previous, ...data}, so "already there" and "arriving now" are the same state). It does not bite here. Validation rules carryevents: ('insert' | 'update')[], and@objectstack/objectqlhonours it — it filters rules byr.events ?? ['insert','update']against the write mode. Soevents: ['insert']expresses the transition gate exactly: historical opportunities on a now-restricted account are never re-evaluated, and acceptance 2's "keep working and keep being editable" holds by construction. The transition-gate/invariant distinction is expressible. Only the cross-object read is not.The decision #1915 needs
The two routes that do satisfy acceptance 2 are not equivalent, and neither is this PR's to choose:
opportunity.hook.tsbeforeInsert, reading the account throughctx.apiand refusing. This is the shape Track A · crm_lead: need type, estimated value, and an approval gate before conversion — REQ-0005 #1914 landed forcrm_lead. It also catches theUPDATEthat re-points an existing opportunity at a restricted account, which route B cannot. It departs from REQ-0003's named construct.crm_opportunity+ avalidations[]entry withevents: ['insert']. Keeps REQ-0003's construct, but needs a new field oncrm_opportunity(which Track A · crm_account: registration identifier, a category that GATES capability, business profile, approval — REQ-0003 #1915 reserves for Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916), needs a writer for the copy, and the copy goes stale the moment an account is reclassified. Strictly dominated by A: anything that can write the copy can simply refuse.Both cost more tokens than this PR already overspends.
What this PR does land
registration_number— the counterparty's registered identity, country-neutral in both name and label. The requirement arrived as 社会信用代码 because that customer registers in China; a Japanese install writes 法人番号, a German one a Handelsregisternummer, a US one an EIN, and aunified_social_credit_codecould be renamed by none of them.searchable, added tosearchableFields, and indexed — acceptance 1. ⛔ Notunique: refusing a save over a shared registered identity is a data-quality policy, and this repo already chose the soft shape once (the hard unique oncrm_lead.email).commercial_capability— the category itself,Full/Settlement Only. Generic values only; REQ-0003 rules the customer's own vocabulary (招标代理公司 …) C. It is a field besidetype, not four more options on it, becausetypeis a relationship-stage axis with two existing readers —opportunity.hook.tsflips a won deal's account tocustomer, so anagencyoption ontypewould be silently overwritten by the first won deal and un-restrict the account.incumbent_vendor,annual_purchasing_budget,payment_cycle, in a newbusiness_profilefieldGroup. Acceptance 3 holds by construction: norecord:detailssection is authored anywhere, so the layout is derived (rung 1 of the AGENTS.md ladder).approval_status+account-approval.flow.ts— anapprovalnode in arecord_changeflow. Ships armed, inverting its lead sibling, because acceptance 4 says so in as many words. The switch is the field'sdefaultValue, ⛔ never the flow'sstatus—draftstill fires triggers andobsoleteis a gate no install can turn back on, both measured in Track A · crm_lead: need type, estimated value, and an approval gate before conversion — REQ-0005 #1914 and quoted rather than re-derived.docs/STATUS.md/README.mdcounts, re-copied from this branch's own validate summary rather than by touching the guard.test/account-approval-gate.test.ts— pins the armed default, the start condition's totality, and the off switch.Not done, and why
content/docs/page. ⛔ Deliberately not written. The page's subject is what the category does, and that is precisely what is undecided; documenting a restriction the product does not enforce would be worse than documenting nothing. The automation-page rows that are accurate did land.Changeset
minor. User-visible metadata: new fields, a new form section and a new approval flow that changes what a newly created account does. Notpatch(nothing here is a fix), notmajor(nothing is removed or renamed — every addition is optional, and no existing account is invalidated: a record with noapproval_statusreads as ungated by the flow'shas()guard).🤖 Generated with Claude Code
https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
Generated by Claude Code