Repository navigation
feat(account): registration identifier, a capability gate, business profile and approval (REQ-0003) - #1948
Merged
os-elon-musk merged 3 commits intoSep 16, 2026
Conversation
…rofile and approval REQ-0003 — the crm_account half of REQ-0002's steps 1, 3, 4 and 5. - registration_number: the counterparty's registered identity, country-neutral by name and label, searchable and indexed. Not account_number, which is our own sequence and already half the record title. - commercial_capability + the rule that reads it: a NEW opportunity may not be opened against a Settlement Only account. Authored as a beforeInsert hook in opportunity.hook.ts reading the account through ctx.api, per the maintainer ruling of 2026-09-16 — the event list IS the transition gate, so historical opportunities keep working and keep being editable (acceptance 2). - Business Profile: incumbent_vendor, annual_purchasing_budget, payment_cycle, in a new fieldGroups entry so the layout stays derived (acceptance 3). - approval_status + account-approval.flow.ts, shipping ARMED because acceptance 4 asks for it — the inverse of its lead sibling, switchable from the field's own defaultValue rather than the flow's status. - Four locale packs, the accounts and automation documentation in en/zh-Hans/ zh-Hant, STATUS.md and README figures re-copied at the source. - REQ-0003's Product response transcribes the ruling that replaced the validations[] construct it named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
…ve from - opportunity-account-capability-gate: the re-point and absent-api legs were describing dispatches the engine never performs. Both now drive the write (the registered beforeUpdate chain, a ctx with no api) rather than calling the handler with a ctx the engine would not hand it. - refusal-envelope: 18 -> 19 swept call sites, the gate's refusal being the new one. The count is hand-maintained on purpose. - docs-readme-token-figures: the docstring band row follows the restated banner (~50k -> ~54k, band 51,300-56,700). Per #1601 the banner pins the measurement and the ceiling stands on its own, so this is a restatement of what the gate reports, not a budget change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
… form sends Reverse verification found the leg green against the construct it exists to rule out: widening the gate's events to beforeUpdate left it passing, because its payload never mentioned the account. A detail form posts every field it rendered, so the ordinary edit on a record sitting on a restricted account carries the restricted link in its own body — that is now the fixture, and it is the case that reddens under the widened events. 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. |
os-elon-musk
marked this pull request as ready for review
September 16, 2026 08:59
This was referenced Sep 16, 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 #1915 — REQ-0003, the
crm_accounthalf of REQ-0002's steps 1, 3, 4 and 5.Supersedes #1931, whose gate construct was overturned by the maintainer ruling below. This branch is re-cut from
origin/main(a3fe341); #1931 is closed.The ruling this PR carries
Maintainer, 2026-09-16 — reply 「其他同意」 to the PM's five-card analysis, of which this card's proposal read, verbatim:
REQ-0003 line 128 named a
validations[]entry oncrm_opportunity. That construct contradicts the same record's acceptance 2, which requires that "opportunities that already point at it keep working and keep being editable": a validation is evaluated against{...previous, ...data}on every write, so "already linked to a restricted account" and "linking now" are the same state, and every later edit to every historical opportunity would be refused.beforeInsertfires only on creation, so the transition gate is the construct rather than a claim about it — which is what the record's own line 126 asks for ("a transition gate, not an invariant").lead.hook.tscarries the identical finding for the lead conversion gate.A second, independent measurement points the same way: a validation predicate cannot read the account at all. It is evaluated over the opportunity's own stored columns,
record.crm_account.<field>does not traverse the lookup (No such key), andos.lookup(...)has no overload because@objectstack/formula'sbuildScope()never bindsctx.api(objectstack#18318) — while an unevaluable predicate rejects the write.ctx.apiis a different surface from that formulaEvalContext.api. It is live on the pinned platform, used ~20 times acrosssrc/sales/objects/*.hook.ts, and pinned against a real engine bytest/hook-query-predicate.test.ts/test/hook-write-shape.test.ts. objectstack#18318 does not block this card.docs/requirements/0003-account-registration-category-and-approval.md's Product response transcribes that ruling, quoting it and the reasoning. No other section of the record is touched.What landed
registration_numberunique, following the soft shape this repo already chose forcrm_lead.email.commercial_capability+ its gateincumbent_vendor,annual_purchasing_budget,payment_cyclein a newfieldGroupsentry, so the layout stays derived — norecord:detailsauthored anywhere (acceptance 3).approval_status+account-approval.flow.tsdefaultValue: 'pending'), the inverse of its lead sibling, because acceptance 4 asks for it in as many words. The off switch is that one default, ⛔ not the flow'sstatus.content/docs/sales/accountsandcontent/docs/administration/automationin en / zh-Hans / zh-Hant (acceptance 5).Why the gate sits beside
typeand not inside ittypeis a relationship-stage axis the system itself advances:opportunity.hook.tsflips a won deal's account tocustomer. Anagencyoption ontypewould be overwritten by the first won deal booked against it, silently un-restricting the account. The two axes are independent — a Settlement Only account can be a prospect, a customer or a former customer.The boundary, stated rather than left to be found
An UPDATE that re-points an existing opportunity onto a restricted account is not refused. That is the ruled construct, and it is what acceptance 2 asks for (the refusal is scoped to "linked to a new opportunity"). It is pinned as a deliberate non-refusal, not left silent.
Tests
pnpm verify— exit 0, the whole chain (validate → typecheck → lint → lint:i18n-gate → hygiene → hygiene:tokens → build → test). Exit codes were redirected to a file before any pipe.Measured at
fcccebd.Reverse verification — both legs predicted, both measured
Each leg: mutate → prove the mutation reached disk (grep counts before/after +
git hash-object) → run → restore → prove the restore (blob equalsHEAD's,git diff HEADempty).Leg A — the construct the ruling rejected.
events: ['beforeInsert']→['beforeInsert', 'beforeUpdate'], i.e. the gate re-evaluated on every write.{ amount }that never mentioned the account — green against the very construct it exists to rule out. A detail form posts every field it rendered, so the ordinary edit on a record sitting on a restricted account carries the restricted link in its own body; that is now the fixture, and it is what reddens. Reported rather than quietly re-run.Leg B — the gate itself neutered (the
settlement_onlyverdict can never match):Token ratchet
⛔ No ceiling was raised, no unrelated prose compressed, nothing hidden in an unmeasured directory.
Baseline
a3fe341was ~52,379 with ~2,621 of headroom; this card spends ~1,906. Measured on the skeleton before the prose was written, as the dispatch required.README banner restated — a measurement, not a budget change
The banner said business semantics ~50k; the gate now measures 54,285, outside the band 47,500–52,500. Per #1601 (「解耦:banner 钉实测,ceiling 独立」) the banner pins the measured reading and the ceiling stands on its own, so the banner is restated from the gate's own
Headlineline — ~54k, band 51,300–56,700 — together with the worked row intest/docs-readme-token-figures.test.ts's docstring that derives from it. ⛔ The guard itself was not touched, and the ceiling did not move.docs/STATUS.mdand the README rosters are re-copied at the source from this branch's ownpnpm validatesummary (343 fields, 31 flows), per the #1917 precedent — ⛔ not by editingtest/docs-declared-versions.test.ts.test/refusal-envelope.test.ts's swept-call-site count moves 18 → 19, the gate's refusal being the new one. That count is hand-maintained by design.Changeset
minor, ⛔ notskip-changeset: six new fields, a new form section, a new approval flow and a new refusal are all user-visible metadata, shipped in every locale pack.Known advisory, deliberate
pnpm validatereports the three Business Profile fields as "carrier-only — declared but nothing in this stack reads or displays it". The liveness diagnostic counts view columns, form sections, page bindings, flow nodes, formulas, hooks and actions as sites; it does not countfieldGroupsmembership — which is exactly the derived layout acceptance 3 demands. ⛔ Answering it with arecord:detailssection would trade acceptance 3 for a quieter log.crm_article_feedback.commentcarries the same verdict onmain. No guard reads this warning; the reasoning is recorded beside the fields so the next agent does not tidy it away.Acceptance notes
commercial_capabilityis no longer inert: the hook reads it, andpnpm validateno longer names it.src/translations/…/src/flows/…paths in locale-pack and object docblocks ([finding] 26 source comments in src/ cite src/flows/ — a directory ADR-0130 removed (blocked on Track A: file surface collides) #1919), every other sales object,src/service|revenue|marketing, andcontent/docs/releases/.opportunity.object.tsis untouched — the gate is a hook, so its fields stay Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916's.Generated by Claude Code