Skip to content

Card 12 (F15 half) — HotCRM hand-off behind CLM_COMPOSITION (M4) #107

Open

Description

@objectstack-fleet

Ruled: 6398591124 · Q2 letter A · Q3 letter A · 2026-10-25T14:01:05Z (Q1 is a platform gap, see Blocked-by)

Filing class: ③ maintainer-directed split of #83 — this card carries F15 of card 12; #83 keeps F8. Ruling-ref: #83 comment 6393744399 (batch #1 item 1 · letter A · maintainer 「同意」), which reads: 「F15 HotCRM 交接与电子签无关,是纯应用元数据,拆出先派」. Reader: the repo:hotclm seat, dispatch once the blocker below lands.

Blocked-by: objectstack-ai/objectstack#25805

Unblocked from the 18.1 upgrade (PR #110, 7b736a0); re-blocked 2026-10-25 on the cross-package reference gap above (os-dev-report on this card).

Card spec: docs/backlog/12-esign-and-crm-handoff.md, the F15 half only — read it in full; this issue carries the increment.

Scope (F15 only)

src/flows/crm-handoff.flow.ts and a resolveComposition() knob (CLM_COMPOSITION=standalone|with-hotcrm, default standalone) mirroring HotCRM's HOTCRM_COMPOSITION — filtered by identity, with a test asserting the filter removed exactly one flow.

Spec — DESIGN.md §06 F15, §08

record_change on crm_contract entering in_approval creates clm_contract (direction sales, party find-or-create from crm_account, amount/dates pre-filled, crm_contract back-link).

Acceptance

  • Gates green in both compositions; test/composition.test.ts pins the flow filter.

Premise the seat re-checks before dispatch

The with-hotcrm composition can be booted for the gate run. How HotCRM's crm_contract is present in that composition is a reading the dev takes from HotCRM's HOTCRM_COMPOSITION precedent; the PM has none.

Out of scope: F8 e-signature (stays on #83, Blocked-by: objectstack-ai/objectstack#25747).


Filed by the repo:hotclm PM seat (session session_017nzWFRUo2U7evMkPdVniAn).

Activity

  1. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked: PR #110 (the 18.1 upgrade) merged as 7b736a0. State pm:blocked → pm:queue; dispatch follows its ruling.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 14
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-107-crm-handoff
    Worktree: hotclm-issue-107
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/flows/crm-handoff.flow.ts (new) · its registration · objectstack.config.ts (resolveComposition(), CLM_COMPOSITION) · test/composition.test.ts (new) and the test runner wiring it needs · src/translations/** keys for the new flow — stop on breach; explain in the report
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: this app's F15 HotCRM hand-off (not built) | none known | every sales team running HotCRM next to the CLM
    Thread-read: 6397115360
    Ruling-ref: #83 comment 6393744399 (batch #1 item 1 · letter A: 「F15 HotCRM 交接与电子签无关,是纯应用元数据,拆出先派」)
    Premise: re-measured by the dev on origin/main 4460d31 (18.1.0); HotCRM's HOTCRM_COMPOSITION precedent read from objectstack-ai/hotcrm (public, read-only clone)
    Serial constraints cleared: PR #110 (18.1) and PR #118 (#20: our_entity is now a required sys_business_unit lookup, which F15's created contract must fill) merged 4460d31. In flight: #38, #99 — disjoint. F8 stays on #83.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 107,
      "status": "needs_decision",
      "branch": "claude/issue-107-crm-handoff",
      "pr": null,
      "session": "session_017nzWFRUo2U7evMkPdVniAn",
      "premise_still_valid": false,
      "summary": "Stopped before writing code. F15 needs a crm_contract back-link on clm_contract, and DESIGN.md §03 pins that back-link as a cross-package lookup. On 18.1.0, os validate refuses such a lookup unless crm_contract is defined in the same stack or in the same artifact's packages[] list. So with-hotcrm cannot be metadata-only. It would have to bundle HotCRM's package stacks into CLM's build, which the dispatch reserves for a maintainer decision. The §03 clm_party.crm_account link, needed to find a party from crm_account, runs into the same refusal. HotCRM's HOTCRM_COMPOSITION precedent does not show otherwise. That knob only chooses between HotCRM's own registrations (default or saas permission sets and seeds) inside one source tree, and shows no app composing with another app. HotCRM is private: true in its package.json and is not on npm (hotcrm, @hotcrm/app and @objectstack/hotcrm all answer 404), so the only way to bundle it is a git dependency on objectstack-ai/hotcrm. Two product inputs are also not specified: where our_entity comes from (it is required) and which contract_type to use (a required lookup; §13 Q6 says 'F15 映射' but the table does not exist, and a fresh install has no types because seeds load only with CLM_DEMO_SEED). These are open_questions 2 and 3. Nothing was pushed and no PR was opened. The worktree is clean at 4460d31. The probe config is kept at /tmp/claude-0/-home-claude/bc53be1b-6c43-5271-bc20-52035f64763b/scratchpad/issue-107/probe.config.ts. The empty-branch push of os-dev rule 1 was skipped: no tracked file was edited, and the relay had 1 write left in the window (39 of 40 used).",
      "tests": "I ran a probe on origin/main 4460d31 with @objectstack/cli 18.1.0. It is an untracked probe.config.ts, now removed. It spreads the real stack, adds Field.lookup('crm_contract') to clm_contract and a record_change flow whose start node is objectName crm_contract with triggerType record-after-update. Run 1: os validate exit=1. Errors: (a) object-reference-unknown on 'object clm_contract · field crm_contract: lookup target crm_contract resolves to no object defined in this stack'; (b) the probe's own `{…}` filter typo, which I fixed for run 2. os lint exit=1 with the same error, plus a warning, flow-trigger-unknown-object ('targets object crm_contract, which this stack does not define'). Run 2 added manifest.dependencies { 'app.objectstack.hotcrm': '^6.0.0', 'app.objectstack.hotcrm.revenue': '^6.0.0' } (the dependency shape HotCRM's own modules use, src/revenue/index.ts:93). os validate still exit=1 with object-reference-unknown on clm_contract.crm_contract, so declaring the dependency does not let validate resolve the lookup. lint exit=1, and the flow-trigger finding stays a warning. Source check: @objectstack/lint 18.1.0 validateObjectReferences resolves only own objects, then artifactProvidedObjectNames(stack) (the packages[] bodies), then PLATFORM_PROVIDED_OBJECT_NAMES. It never reads manifest.dependencies. os explain flow-trigger-unknown-object says the runtime publish gate DOES judge a package against its manifest.dependencies closure. So the platform resolves names against dependencies when a package is published, but not at build time. Earlier history: card 02 left the lookup out under objectstack#16611 (closed as completed); that fix is what made this an error. No composition test, ablation or boot was run, because no code was written. NOT MEASURED: a with-hotcrm boot end to end. Reason: CLM can only boot with crm_contract present by bundling HotCRM's stacks into its artifact, which is decision Q1-B.",
      "gates": "No change was made, so the four gates were not run on a diff. The only local readings are the probe exits in tests (validate exit=1 and lint exit=1, each captured to its own log before any grep). The PR's Validate check-runs: NOT MEASURED, because no PR exists. The objectstack dispatch-gates.mjs derivation does not apply to hotclm.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (no PR)",
      "mcp_calls": "0",
      "api_writes": "1 — post-stamped comment POST /repos/objectstack-ai/hotclm/issues/107/comments (this report). No push, no pr_create.",
      "open_questions": [
        {
          "question": "Q1 (blocking, architecture). How does CLM get its §03 cross-package links, clm_contract.crm_contract and clm_party.crm_account, when 18.1 validate refuses any lookup to an object outside the stack or the artifact's packages[] list, even with HotCRM declared in manifest.dependencies?",
          "options": [
            "A. Platform route: file an objectstack issue asking that the build-time object-reference ladder (validate, lint, build) also resolve names against the packages declared in manifest.dependencies. The runtime publish gate already judges a package against that closure (os explain flow-trigger-unknown-object). #107 then waits with Blocked-by on it. After the fix, F15 is metadata-only and stays size M: one manifest dependency, the flow, and the two lookups registered only under with-hotcrm. Cost: platform work, plus waiting.",
            "B. Bundle route: under with-hotcrm, import HotCRM's four package stacks into CLM's artifact (composeStacks with manifest 'preserve'). HotCRM is private and not on npm, so this means a git dependency on objectstack-ai/hotcrm. It brings HotCRM's whole runtime closure (19 @objectstack packages at 18.1.0), ties every CLM release to a HotCRM commit, and makes CLM's build ship a second app. This is a capability expansion, which AGENTS.md reserves for the card or the maintainer. Benefit: a with-hotcrm boot becomes possible locally now. Cost: about L.",
            "C. Text route: store the CRM ids as text fields, not lookups. This changes the governed §03 field types, loses the record picker and $expand on both links, and duplicates an id across the seam that F9's write-back then trusts. No platform wait. Cost: S, plus a §03 edit."
          ],
          "recommendation": "A. A reference across apps is a platform capability, by the maintainer's rules 「本项目是元数据应用,平台相关的功能应该在平台开发」 and 「平台的问题就等平台」. The platform already holds the dependency closure at publish time, so the gap is narrow and mechanical. B makes a CLM build carry HotCRM, the opposite of DESIGN §01 'not a HotCRM module'."
        },
        {
          "question": "Q2 (product). clm_contract.our_entity is a required lookup to a company-kind sys_business_unit, and a crm_contract carries no signing entity. What should F15 fill it with?",
          "options": [
            "A. The CRM contract owner's sys_user.primary_business_unit_id, walked up parent_business_unit_id to the nearest active company-kind unit. If none is found, F15 creates nothing and notifies the owner. This is an honest source (the entity that employs the deal owner) and works for the multi-entity groups DESIGN §01 targets. Cost: a bounded ancestor walk in the flow (get_record steps or a loop node), since script functions cannot read data.",
            "B. The single active company-kind unit in the tenant, with refuse-and-notify when there are zero or several. Cheapest option, but every hand-off fails in a group with more than one entity, the case §01 calls out (集团多法人).",
            "C. A sys_setting 'signing entity for CRM hand-offs', with refuse-and-notify when unset. Simple and explicit, but it is a new settings key and stays a single entity per tenant."
          ],
          "recommendation": "A, with B's refuse-and-notify behaviour when the walk finds nothing. It is the only option that is both derived from real data and right for a multi-entity group."
        },
        {
          "question": "Q3 (product). clm_contract.contract_type is a required lookup, and §13 Q6 rules that F15 maps HotCRM's commercial type (subscription, service, license, partnership, nda, msa) without stating the map. A fresh install also has no clm_contract_type rows, because seeds load only with CLM_DEMO_SEED. How does F15 choose the type?",
          "options": [
            "A. A fixed category map in the flow: nda to nda; msa to framework; service to service; subscription, license and partnership to sales. F15 takes the one active type with direction sales and that category, and refuses and notifies when there are none or several. The map is part of the standard product.",
            "B. A sys_setting naming one default CLM type for every hand-off, with refuse-and-notify when unset.",
            "C. Add a crm_contract_types multiselect on clm_contract_type so each customer configures the match as data. This is a §03 schema change."
          ],
          "recommendation": "A, once the maintainer confirms the six-row map, because §13 Q6 already chose a mapping inside F15. The axes framework was not in the dispatch, so these options give cost and recommendation in plain form; the seat should reframe them on its axes before posting the decision card."
        }
      ],
      "out_of_scope_findings": [
        "observation · crm_contract has no currency field (contract_value only), so CLM's currency_code cannot be pre-filled from the CRM; the honest fill is the field's own default with is_amount_estimated left false. This is noted for whoever implements F15 after Q1 to Q3 are answered · carrier: #107",
        "observation · src/flows/index.ts line 15 still reads 'F8 (e-signature) and F15 (CRM hand-off) arrive with cards 12 and 13', but both are card 12 (F15 is now #107, F8 is #83). This is a stale comment and is not filed · carrier: none, recorded here only"
      ]
    }
  4. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat note · 2026-10-25T13:54Z — dev report above, read by the seat.


    Generated by Claude Code

  5. zhuangjianguo commented on Oct 25, 2026

    @zhuangjianguo
    Collaborator

    Ruling: batch #7 items 1–2 · Q2 letter A · Q3 letter A · maintainer 「同意」 2026-10-25T14:01:05Z

    Decision-box doc: https://claude.ai/artifact/D63mjEkdjhEoE3AdzYih1V. Q1 (18.1 build-time validate ignores manifest.dependencies for cross-package lookups) is a platform gap, not a ruling: Blocked-by: objectstack-ai/objectstack#25805 as the body says; no workaround. Freshness: os-dev-report 6398429188 (13:46Z), body re-blocked 13:55Z; re-read before recording, the two questions unchanged.

    Q2 — our_entity for a handed-off contract: A, derived from data

    1. Take crm_contract.owning_business_unit_id (the platform-injected ownership column, ADR-0117 D1; it is not declared in HotCRM source and will not grep there). If empty, take the CRM contract owner's sys_user.primary_business_unit_id.
    2. Walk parent_business_unit_id upward to the nearest active kind = company unit, bounded (a loop / get_record chain; script nodes cannot read data).
    3. Found ⇒ our_entity = that unit. Not found ⇒ F15 creates nothing and notifies the CRM contract owner, in the refuse_* shape the backfill flow already uses.

    No new sys_setting, no single-entity assumption (DESIGN §01 集团多法人).

    Q3 — contract_type for a handed-off contract: A, a fixed category map in the flow

    HotCRM crm_contract.contract_type CLM clm_contract_type.category
    nda nda
    msa framework
    service service
    subscription sales
    license sales
    partnership sales

    The six HotCRM values were verified by the seat on hotcrm main 8f52859 (src/revenue/objects/contract.object.ts:174-184). The maintainer confirmed the six rows as written.

    Selection rule, corrected from the dev's draft: among active clm_contract_type rows whose category matches, with direction in {sales, other} (never purchase), exactly one ⇒ use it; zero or several ⇒ refuse and notify. The dev's "direction = sales" alone would refuse every CRM NDA, because the shipped seed's NDA type is direction: 'other' (src/data/plan.ts; on the seed the map lands nda→NDA, msa→MSA, service→SOW, subscription/license/partnership→ORD, one each). A fresh install with no types refuses every hand-off until types exist, which is the honest outcome.

    Lane follow-through

    • Both answers go into crm-handoff.flow.ts when #25805 lands; the map is standard-product metadata, not configuration.
    • DESIGN §13 Q6 (governed): the row 「不对齐,F15 映射」 gains a pointer to this table (the seat's design-doc PR, Refs #107).
    • The dev's out-of-scope note stands: crm_contract has no currency, so currency_code keeps the field default and is_amount_estimated stays false.

    ⛔ The director does not change labels; the lane seat transitions with Ruling-ref:.


    Generated by Claude Code

  6. added 3 commits that reference this issue on Oct 25, 2026
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