Repository navigation
docs(contracts): write manual termination and sharing against the profiles and the status machine (#872) - #878
Merged
Conversation
…files and the status machine (#872) Three sentences on the Contracts page contradicted what PR #874 measured on the same page, in all three languages. - "The owner can change status to Terminated" is false for the most common owner: edit on crm_contract is object-level and ownership does not extend it. sales-rep.profile.ts grants allowEdit: false, and quote_on_accepted copies the quote's owner_id onto the contract it drafts, so the rep owns a record they can read and cannot touch. Sales Manager and System Administrator hold edit. - "Terminated contracts can't be reactivated" is a gate nobody built: contract_status_progression declares terminated a dead end at warning severity, and a non-error verdict is logged while the save proceeds. The one-way advice is kept, but the page now says nothing enforces it. - "The contract owner reads and edits their own contracts" conflated the two access layers: record-level sharing decides which records you reach, the profile's object-level CRUD decides whether you hold edit at all. Documentation only. No metadata, profile, permission or validation-rule changes. Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Author
|
[PM 更正] 本 PR 正文「Two findings filed separately」段引用的编号有误:文中写的 #875 / #876 实为并行 dev(#869 单)先行占用的编号(
以更正后的编号为准。 Generated by Claude Code |
This was referenced Aug 6, 2026
yinlianghui
added a commit
to yinlianghui/hotcrm
that referenced
this pull request
Aug 10, 2026
…ach (objectstack-ai#881) (objectstack-ai#893) The closing paragraph of the *Sharing* section on `content/docs/revenue/contracts.mdx` was rewritten in objectstack-ai#699 (0899b4f) against the reach measured in objectstack-ai#694: neither route an admin can take actually delivers "contracts follow the account" today — a sharing rule on Contract widens the records it matches for every holder of the object, and a Controlled-by-Parent OWD derives org-wide in this release, the parent link not being consulted per caller. That commit touched the English page only. Both Chinese pages stayed on the pre-correction text, which said the two routes merely "open contract visibility to every user of the object" and omitted the conclusion the correction exists to deliver. `contracts.zh-Hans.mdx` and `contracts.zh-Hant.mdx` now carry the same three facts as the English page, and cite the *Controlled by Parent, in practice* section by its translated heading — “由父级控制”在实践中 / 「由父層控制」在實務中 — on top of the page-level link they already had. No new anchor link: the translated headings do not slugify to the English anchor, so the zh pages keep the un-anchored form used since objectstack-ai#868, and the English page cites that section as plain text as well. Translation only — no new assertion beyond what the English page states. No English page, metadata, profile, sharing-rule or OWD change; the reach being described stays pinned by `test/parent-derived-reach.test.ts`. The first two bullets of the same section, rewritten in objectstack-ai#878, are untouched. Fixes objectstack-ai#881 Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #872
Three sentences on the Contracts page, rewritten against
src/and the engine, in all three languages. Documentation only —src/**untouched, no guard added.Premise re-verification (baseline includes PR #874)
This PR is cut from
6cd53d2e, which is #874's merge. All three sentences were relocated on that baseline and re-measured; all three premises hold.:120-121:124:120-121:124:145src/profiles/sales-rep.profile.ts:52grantscrm_contractwithallowEdit: false(read-only,readScope: 'own'). Onlysales-manager.profile.ts:24andsystem-admin.profile.ts:28hold edit; the other four profiles do not grant the object at all. The owner is very often a rep:src/objects/quote.hook.ts:141-143copies the quote'sowner_idonto the contractquote_on_acceptedinserts.src/objects/contract.object.ts:258-273—contract_status_progression,severity: 'warning',terminated: []. Engine re-read atnode_modules/@objectstack/objectql/dist/core.mjs:2434-2441: onlyseverity === "error"pushes ontoerrors(which then throwsValidationError); every other level takes theelsebranch, logs a warning, and the loop continues. The save proceeds.@objectstack/plugin-securitymapsupdatetoallowEditand denies unless a permission set grantsallowEditormodifyAllRecords(dist/index.mjs:1742-1814). Record gate:@objectstack/plugin-sharing'scanEdit(dist/index.mjs:1978-2003) — ownership within write depth, or an explicit write-level share. Its comment on the neighbouringcanDeletestates the direction outright: "sharing widens rows, never verbs".crm_contractissharingModel: 'private'(contract.object.ts:19) andsrc/sharing/carries no contract rule.The subsection's third sentence — "Audit log records who and when" — was checked and kept:
statuscarriestrackHistory: true(contract.object.ts:95), as doesowner_id(:45). The rewrite grounds it rather than dropping it.Before and after
1 + 2 — Manual termination (
:123-124→:123-129). Before (EN): "The owner can change status to Terminated. This is a one-way action — terminated contracts can't be reactivated. Audit log records who and when." After: who may terminate is an object-level right that ownership does not extend; the gap lands on the common case because the accepted-quote hook stamps the rep as owner; and the dead end is declared at warning severity, so a revival is logged and saved anyway. Per the dispatch the advice semantics of "one-way" are kept — it is what the status means and how everything downstream reads it — but the page now says plainly that nothing enforces it and that enforcement is an admin's job.zh-Hans and zh-Hant carry the same three paragraphs. Status names keep the form this subsection and its neighbours already use (
*Terminated*/*Activated*italic); the CJK status vocabulary stays in Built-in rules, where #874 established it.3 — Sharing (
:145-146→:151-152). Before (EN): "The contract owner reads and edits their own contracts" / "…edits stay with the owner except for admins." After, the first bullet separates the layers — ownership buys read, and whether you hold edit at all is settled earlier by the profile's object-level rights, so no sharing rule and no record share turns that read into an edit. The second bullet's trailing clause had to move with it: once the first bullet says the owner cannot edit, "edits stay with the owner" is a fresh self-contradiction on the same list. It is now a plain permission-table statement.How this sits with #874 / #831
Nothing either PR wrote was reverted; the new text echoes without restating.
:137(docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874) — "The status progression is advice, not a gate." carries the mechanism at length. Termination does not repeat it: it states the consequence for this transition and points at that bullet by name.:139(docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874) — "nothing stops an activated contract from being deleted": same defect class, and the closing move is deliberately parallel ("an admin has to author that"). Delete and edit stay distinct — Sales Manager holds edit and holds no delete, which both passages now say consistently.:172/:174(docs(contracts): 按视图、选项集、hook 与 profile 写实合同页其余四处 (#832) #874) — rep-facing advice. The new Sharing bullet is the mechanism behind it (object gate vs record gate), at a different altitude; termination ends on the same referral without re-teaching it.:153(docs(contracts): 按到期/续约 flow 与 contract.object.ts 重写三语剩余三处失真 (#826) #831/docs: controlled_by_parent 可达范围按实测改写(#694 本仓侧纠偏) #699) — the "follows the account" paragraph is untouched.Verification
Shared lock plus a 4 GB heap, all in this worktree.
pnpm validate✓ Validation passed (2407ms)·Data: 17 Objects 344 Fields· 5 pre-existing warnings, unchangedpnpm typechecktsc --noEmit, no outputpnpm build✓ Build complete (1486ms)·Artifact: dist/objectstack.json (1921.4 KB)pnpm test -- --maxWorkers=2Test Files 66 passed (66)·Tests 1587 passed, 1 skipped (1588)pnpm lint13 warning(s), 14 suggestion(s)— all pre-existingpnpm hygiene✓ no raw control bytes in first-party files·✓ source hygiene cleanThe
✗ source hygiene: scanned director(y|ies) missinglines inside the test run are that meta-test's own expected stderr.Control bytes: this repo's scan is
scripts/check-source-hygiene.mjs(viapnpm hygiene, covering 397 files undercontentand.changeset) — there is nocheck-nul-bytes.mjshere. An independent scan of the four changed files for0x00-0x08,0x0b,0x0c,0x0e-0x1freturned zero matches.Prose only, so no pin test, and no
@objectstack/*upgrade.Filed separately, not fixed here
allowEditoncrm_contractbut nowriteScope, withmodifyAllRecords: false, so the effective write depth isown; with no contract sharing rule anywhere, a manager cannot edit a contract a rep owns — only a System Administrator can. Same failure shape as Seeded contracts are ownerless at the platform level (owner_idnull) — nobody, admin included, can edit one #622/campaign_leadership_* 的论证注释把 crm_campaign 说成 private-OWD,实际是 public_read —— 规则本身仍有用,理由是错的 #724 on two other objects. This is why this PR's wording stays at the object-gate altitude ("hold edit on it") instead of claiming a manager can terminate any contract. It does mean:174's referral is optimistic, which is for triage.On #549: this PR describes only what is measured today. If #549 settles on a different account for the read side, this bullet should move with it.