Skip to content

The contract page's new-version form defaults "Current" off, and two versions of one contract can both be current #96

Description

@objectstack-fleet

Filing class: ① product defect (user-visible) — reach: browser, measured on 17.7.0. Reader: the repo:hotclm seat, dispatch from pm:queue.

Source: the #87 full browser pass on 17.7.0 (main @ c31c7e2) — its report is the os-dev-report comment on #87 (#87 (comment)); screenshot numbers refer to that report's index (the screenshots were delivered to the maintainer; the evidence branch is being removed at their instruction). Finding 16; screenshots 079–085.

Symptom

  • The version form on the contract page defaults is_current to off, so a clean version saved as offered (079, 082) does not satisfy the signing guard ("A current clean version is required before signing.").
  • After ticking it, versions 1 (draft) and 3 (clean) were both is_current: true: nothing keeps "current" exclusive per contract.

Expected (verify against DESIGN.md §03 clm_contract_version)

A new version is current by default when it is the newest, and marking one current un-marks the others (in a hook, so every door obeys it). If §03 does not pin which, stop with needs_decision.

Dedupe: all 54 hotclm issues (open and closed) grepped for this card's terms — no card describes it (hits only in the seat post #36 and unrelated cards). (terms: is_current)


Filed by the repo:hotclm PM seat from the #87 browser pass.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01HihZ11bQSqjCgjzHbpv4M1
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-96-current-version
    Worktree: hotclm-issue-96
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/objects/contract-version.object.ts · a new src/objects/contract-version.hook.ts (and its registration in src/objects/index.ts) · src/pages/contract_detail.page.ts (the version form's default, if that is where it lives) · src/translations/** (only for a new message) — ⛔ not src/objects/contract.hook.ts (held by PR #91) — stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo)
    Clause-②: no
    Responsibility: this app's version object and its hooks | none | legal and records adding versions on every contract page
    Thread-read: none
    Serial constraints cleared: PR #91 holds contract.hook.ts · contract-intake.flow.ts · contract-lifecycle.actions.ts; #93 holds contract-approval.flow.ts · README.md · scripts/demo.mjs; #97 holds the renewal / expiry flows — all outside this surface
    Read at: 2026-10-10T04:04Z


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 96,
      "status": "needs_decision",
      "branch": "claude/issue-96-current-version",
      "pr": null,
      "session": "session_01HihZ11bQSqjCgjzHbpv4M1",
      "premise_still_valid": true,
      "tree": "46e65f0",
      "platform": "@objectstack/* 17.7.0 (pnpm-lock unchanged since the #87 measurement tree c31c7e2)",
      "summary": "Stopped before code, as Zone 1 and the card both rule: DESIGN.md section 03 does not pin either half. The clm_contract_version row lists only `is_current boolean`, and the approved → signing guard reads 「存在 `kind: clean` 的当前版本」. That says nothing about how many versions may be current, and nothing about a default. The governed proposal docs/design/00 section 5.5 doesn't even require 'current': its guard is 「送签必须存在一份清稿」 / 「存在清稿版本」. The premise holds on origin/main 46e65f0 and the default is app metadata, not a Console defect. Two questions with options and a recommendation are below. No PR, no files changed, nothing filed.",
      "premise_evidence": [
        "Form default = app metadata: src/objects/contract-version.object.ts:83-88 declares `is_current: Field.boolean({ defaultValue: false })`. The page's Versions tab (src/pages/contract_detail.page.ts:265-274) is a bare record:related_list with no form defaults, so the platform form shows exactly what the object declares. This is not a Console defect, and there is no platform cause to report.",
        "Exclusivity is not enforced anywhere: the only hook on clm_contract_version is `contract_version_display_name` (src/objects/mirror.hook.ts, it stamps display_name only). src/objects/hooks.ts registers contract.hook + mirror.hook and nothing else. `grep -rn is_current src` finds no writer that un-marks siblings.",
        "#87 measurement reapplies byte-for-byte: `git diff --stat c31c7e2 origin/main` = src/profiles/requester.profile.ts only (#88, sys_comment grants). The version object, page, hooks, flows and pnpm-lock are identical to the tree the #87 browser pass measured (finding 16, screenshots 079-085).",
        "Flows' deliberate writes: contract-intake.flow.ts:375/395/410 write v1 `is_current: true` (the only version). executed-upload.flow.ts:268 (F16 backfill) writes v1 final_signed `is_current: true` (the only version). signature-record.flow.ts:308-314 (F7) writes final_signed `is_current: false` on purpose: 'the negotiation flag is what approved → signing reads … DESIGN.md section 03 keeps signing → approved open for a failed signing round — moving the flag onto the executed copy would make that rollback unre-enterable.'",
        "Caller rights for an un-marking write: clm_requester and clm_records hold create without edit on versions (src/profiles/requester.profile.ts:70, records.profile.ts:35). clm_legal holds edit (legal.profile.ts:56). An exclusivity rule must therefore un-mark siblings as a system write. In-repo precedent: `deviation_gate` (contract.hook.ts:819-846) runs afterUpdate with `runAs: 'system'` and writes the parent from outside its hook chain.",
        "Downstream reader that needs one answer: docs/backlog/12-esign-and-crm-handoff.md:13 has F8 post 'the current `clean` version' to the e-sign provider, i.e. one document."
      ],
      "tests": "No code was written, so no tests ran. Premise verification commands: `git rev-parse origin/main` → 46e65f0a66e4; `git diff --stat c31c7e2 origin/main` → '1 file changed, 29 insertions(+)' (src/profiles/requester.profile.ts); `grep -rn is_current src --include=*.ts` (12 hits: 10 in objects/flows/page, 2 in the en and zh-CN bundles). The browser re-run on 46e65f0 is NOT MEASURED, reason: the result is a decision, and the #87 browser measurement's inputs are byte-identical on this tree (see premise_evidence).",
      "gates": {
        "pnpm verify": "NOT RUN, reason: no diff (needs_decision, files_changed empty); a gate run here would measure origin/main, not a change"
      },
      "line_budget": "n/a",
      "files_changed": [],
      "deviations": [
        "Skipped `pnpm install` in worktree hotclm-issue-96: the stop came before any code, and nothing in the stop needs node_modules. The worktree was removed clean after this report.",
        "The branch claude/issue-96-current-version was pushed empty (equal to origin/main 46e65f0) as the claim's landing marker and is left on the remote for the seat to keep or delete."
      ],
      "mcp_calls": "0",
      "api_writes": "1: this report comment on objectstack-ai/hotclm#96, posted through scripts/pm/post-stamped.mjs (fleet relay). The branch push is git, not REST. Reads: GET issues/96, GET issues/96/comments, GET issues/comments/6092819369 (the #87 report).",
      "open_questions": [
        {
          "question": "Q1: Is 'current' exclusive per contract? (section 03 lists `is_current boolean` and nothing more)",
          "options": [
            "A: At most one current version per contract, enforced by a new src/objects/contract-version.hook.ts (afterInsert/afterUpdate, `runAs: 'system'`): when a row becomes current, every other current row on the same contract is un-marked. This binds every door: form, REST, flows, MCP. Business need: the field's own help text reads 'the version negotiation is currently on' / 「谈判当前所在的版本」, which is singular. F8 has to send ONE clean file to the e-sign provider. The #87 state (draft v1 + clean v3 both current) leaves 'which file gets signed' unanswered. Long-term: one invariant in one hook, and F8 reads 'the' current version without inventing a tie-break. AI-error: an agent adding a version through MCP/REST cannot leave two currents behind, and it does not need to know to un-tick the old one. Startup-focus: one hook file plus one line in hooks.ts, with no schema change and no seed change (no versions are seeded, src/data/index.ts:33). Cost: the un-marking must be a system write, because requester and records cannot edit versions (see precedent `deviation_gate`). Section 03 needs one sentence, a governed edit that is yours to make.",
            "B: Not exclusive. 'Current' is a per-version tag, and the guard keeps reading 'any current clean version'. No code, and the #87 state becomes legal by definition. Business need: the Versions timeline shows several 'Current' rows. Long-term: F8 (card 12) needs its own choosing rule, so the cost moves to card 12. AI-error: high, because nothing tells a writer the flag means anything. Startup-focus: cheapest today.",
            "C: Retire `is_current`. 'Current' becomes the highest-numbered version that is not final_signed, and the guard becomes 'the newest negotiation version is clean'. No flag to get wrong, and it matches docs/design/00 section 5.5's guard (「存在清稿版本」, no 'current'). Cost: a section 03 field removal, rewriting the intake/F16/F7 writes, the page column and both translation bundles. It also loses the ability to sign an earlier clean copy after a later counterparty redline is rejected (legal would re-upload it as a new version)."
          ],
          "recommendation": "A. Business need: one version is what the field already says it means, and what F8 needs to send. Long-term: a single hook makes every door obey the rule, where B pushes the ambiguity onto card 12 and C removes a capability. AI-error: A is the only option where a writer that knows nothing about the flag still leaves a coherent contract. Startup-focus: A is a ~40-line hook with no schema or seed change, against C's multi-file removal. Proposed section 03 wording for the clm_contract_version row: 「is_current boolean(每份合同至多一个当前版本:标记一个即取消其余,hook 强制,各入口一致)」."
        },
        {
          "question": "Q2: Does a new version default to current? (section 03 is silent; today the field declares `defaultValue: false`, and the #87 pass hit exactly that)",
          "options": [
            "A: Yes for every negotiation kind (draft, internal_redline, counterparty_redline, clean); `final_signed` never takes the flag from a negotiation version. Mechanism for the implementer: field `defaultValue: true`, so the form offers it ticked and a write that omits the key stores true. In the Q1 hook, a final_signed row keeps `is_current: true` only when it is the contract's only version (F16's backfill case), and is otherwise stored false. F7's explicit false, intake's true and F16's true all keep their meaning, and signing → approved stays re-enterable. Business need: legal saves a clean version as offered and Start Signing passes, which is the #87 path. Long-term: the newest negotiation round is what the contract is on. AI-error: low, because the writer does nothing special. Startup-focus: a one-line field change plus a few lines in the same hook.",
            "B: Yes for every kind (`defaultValue: true` only, no kind rule). Simplest. The cost: an executed copy added through the version form (records hold create on versions, 「执行副本」) would take the flag off the clean version under Q1-A. That is exactly what F7's comment protects against: after a failed signing round is rolled back to approved, signing could not be re-entered until someone re-ticks the clean version.",
            "C: No, keep `defaultValue: false` and legal ticks it. The #87 symptom stays: a clean version saved as offered does not satisfy the guard. It can only be softened by rewording the refusal ('mark the clean version current'). Cheapest, but the trap stays."
          ],
          "recommendation": "A. Business need: it removes the #87 dead end (save as offered → guard refuses) without asking legal to know about a flag. Long-term: it keeps F7's deliberate design, where the executed copy never displaces the negotiation version, and B breaks that. AI-error: a default that is right for every negotiation write means an agent that omits the key still gets the correct state. Startup-focus: it lives in the same hook file as Q1-A, plus one field line. If section 03 takes the wording, append to the Q1 sentence: 「新的谈判版本默认为当前;final_signed 不夺取当前标记」."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the maintainer, through this decision · noted, not filed. DESIGN.md section 03's approved → signing guard reads 「存在 `kind: clean` 的当前版本」, while the governed proposal docs/design/00-设计方案.md section 5.5 / its state table reads 「送签必须存在一份清稿」 / 「存在清稿版本」 (no 'current'). Q1-C would align the two; A and B leave docs/design/00 as the looser statement. A governed-document wording drift, outside classes a/b/c. Dedupe words: is_current, 当前版本, clean version guard"
      ],
      "platform_causes": "none: the form's 'Current off' default is the app's own `defaultValue: false`, and the Console honours it. Nothing filed, per the lane ruling."
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    决策:「当前版本」是否每份合同唯一?新版本是否默认为当前? · 2026-10-10T04:13Z

    Governing text: DESIGN.md §03(clm_contract_version 行只写 is_current boolean;状态机 approved → signing 守卫「存在 kind: clean 的当前版本」)· §06 F7(签署记录把执行副本写为 is_current: false,以保留签署失败回退后可再次进入签署)· docs/backlog/12(F8 要把「当前 clean 版本」这一份文件发给电子签服务商)

    维护者速读

    事情:浏览器测试(#87)里,法务在合同页按默认值存了一份清稿,「发起签署」仍被守卫拒绝——因为版本表单把「当前」默认关闭(这是应用自己的 defaultValue: false,不是平台问题);勾上之后,同一合同的草稿 v1 与清稿 v3 同时是「当前」,没有任何规则保证唯一。dev 读完 §03 后停下:§03 两件都没规定,这是你的产品语义(且 §03 属受管文本)。

    你要做的:回两个字母,如「1A 2A」。

    问题 1:「当前」是否每份合同至多一个?

    做法 代价 / 收益
    1A(推荐) 至多一个:新增版本对象的 hook,标记一个即以系统身份取消其余,所有入口(表单 / REST / 流程 / MCP)一致 与字段帮助文字「谈判当前所在的版本」一致;F8 只发一份文件无需另立规则;约 40 行 hook,无 schema / 种子改动;§03 需加一句(受管,由你写)
    1B 不唯一:「当前」只是每个版本的标签 今天零成本;但「到底签哪一份」推给卡 12 解决
    1C 取消 is_current:当前 = 编号最大的非执行副本版本 没有可写错的标记;但要删 §03 字段、改三个流程与页面与两套翻译,并失去「回签较早清稿」的能力

    问题 2:新版本是否默认为当前?

    做法 代价 / 收益
    2A(推荐) 谈判类版本(草稿 / 内部修订 / 对方修订 / 清稿)默认当前;执行副本(final_signed)不夺取当前标记(唯一版本时除外,即补录场景) 法务按默认保存即可发起签署(#87 的死路消失);保留 F7 的刻意设计
    2B 所有版本一律默认当前 最简单;但档案岗新增执行副本会把标记从清稿上拿走,签署失败回退后无法再次进入签署——正是 F7 注释所防的
    2C 保持默认关闭,由法务手动勾选 零改动;#87 的陷阱继续存在

    若选 1A + 2A,dev 建议的 §03 措辞:「is_current boolean(每份合同至多一个当前版本:标记一个即取消其余,hook 强制,各入口一致;新的谈判版本默认为当前;final_signed 不夺取当前标记)」。

    四棱分析

    • 实际业务需求:真实且已测——Full browser test on 17.7.0 main: drive the whole contract lifecycle as every audience, in en and zh-CN, and report what a real user hits #87 中按默认保存的清稿过不了签署守卫;F8(卡 12)必须知道发哪一份。
    • 项目长远合理性:1A 把不变量放在一个 hook 里、所有入口遵守;1B 把歧义推给后续卡;1C 删能力。
    • 防 AI 写错:1A + 2A 下,一个对这个标记一无所知的写入者(agent 经 MCP / REST 添加版本)也会留下一致的合同;1B / 2C 依赖写入者「知道要去勾」。
    • 创业阶段不扩散:1A + 2A 是一个 hook 文件加一行字段默认值,无 schema、无种子改动。

    另记(不需裁):docs/design/00 的守卫写的是「存在清稿版本」(无「当前」),与 §03 措辞有出入——属受管文档措辞漂移,随本决策一并由你定稿即可。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions