Skip to content

docs(contracts): write manual termination and sharing against the profiles and the status machine (#872) - #878

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-872-contracts-termination-sharing
Aug 6, 2026
Merged

yinlianghui merged 1 commit into
mainfrom
claude/issue-872-contracts-termination-sharing

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

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.

# Sentence Issue said On this baseline (same line in all 3 locales) Premise
1 "The owner can change status to Terminated" :120-121 :124 holds
2 "terminated contracts can't be reactivated" :120-121 :124 holds
3 "The contract owner reads and edits their own contracts" (PM's addition) — :145 holds
  • 1. src/profiles/sales-rep.profile.ts:52 grants crm_contract with allowEdit: false (read-only, readScope: 'own'). Only sales-manager.profile.ts:24 and system-admin.profile.ts:28 hold 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-143 copies the quote's owner_id onto the contract quote_on_accepted inserts.
  • 2. src/objects/contract.object.ts:258-273 — contract_status_progression, severity: 'warning', terminated: []. Engine re-read at node_modules/@objectstack/objectql/dist/core.mjs:2434-2441: only severity === "error" pushes onto errors (which then throws ValidationError); every other level takes the else branch, logs a warning, and the loop continues. The save proceeds.
  • 3. Two independent gates. Object gate: @objectstack/plugin-security maps update to allowEdit and denies unless a permission set grants allowEdit or modifyAllRecords (dist/index.mjs:1742-1814). Record gate: @objectstack/plugin-sharing's canEdit (dist/index.mjs:1978-2003) — ownership within write depth, or an explicit write-level share. Its comment on the neighbouring canDelete states the direction outright: "sharing widens rows, never verbs". crm_contract is sharingModel: 'private' (contract.object.ts:19) and src/sharing/ carries no contract rule.

The subsection's third sentence — "Audit log records who and when" — was checked and kept: status carries trackHistory: true (contract.object.ts:95), as does owner_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.

Verification

Shared lock plus a 4 GB heap, all in this worktree.

Step Exit Key output
pnpm validate 0 ✓ Validation passed (2407ms) · Data: 17 Objects 344 Fields · 5 pre-existing warnings, unchanged
pnpm typecheck 0 tsc --noEmit, no output
pnpm build 0 ✓ Build complete (1486ms) · Artifact: dist/objectstack.json (1921.4 KB)
pnpm test -- --maxWorkers=2 0 Test Files 66 passed (66) · Tests 1587 passed, 1 skipped (1588)
pnpm lint 0 13 warning(s), 14 suggestion(s) — all pre-existing
pnpm hygiene 0 ✓ no raw control bytes in first-party files · ✓ source hygiene clean

The ✗ source hygiene: scanned director(y|ies) missing lines inside the test run are that meta-test's own expected stderr.

Control bytes: this repo's scan is scripts/check-source-hygiene.mjs (via pnpm hygiene, covering 397 files under content and .changeset) — there is no check-nul-bytes.mjs here. An independent scan of the four changed files for 0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f returned zero matches.

Prose only, so no pin test, and no @objectstack/* upgrade.

Filed separately, not fixed here

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.

…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>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 6, 2026 2:34am

Request Review

Copy link
Copy Markdown
Contributor Author

[PM 更正] 本 PR 正文「Two findings filed separately」段引用的编号有误:文中写的 #875 / #876 实为并行 dev(#869 单)先行占用的编号(src/docs/crm_sales.md 经理声明 / sla-and-escalation 升级描述)。本 PR 实际立的两单是:

以更正后的编号为准。


Generated by Claude Code

Merged via the queue into main with commit 0550c5f Aug 6, 2026
9 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

contracts.mdx「手动终止」小节两句都不成立:所有者未必能改状态,已终止的合同也拦不住被复活

1 participant