Skip to content

Post-signature domain: clm_obligation, clm_payment_plan, and the contract roll-ups (card 03, M1) #3

Closed

Description

@hotlong

Milestone: M1 · Card: docs/backlog/03-post-signature-domain.md
Blocked-by: #2

Scope

src/objects/obligation.object.ts, payment-plan.object.ts; the five summary roll-up fields on
clm_contract; state-machine guards for both children in contract.hook.ts.

Spec — DESIGN.md §03

Both controlled_by_parent, masterDetail clm_contract cascade, nameField: 'display_name'
(mirror stamped by mirror.hook.ts: obligation = title; payment plan = #<seq> · <planned_date>; a localized
form such as 第<seq>期 belongs to the zh-CN bundle, not the stored mirror).
clm_obligation icon list-checks; clm_payment_plan icon banknote. Amounts currency scale 2 min 0.

Roll-ups on clm_contract (Field.summary): version_count count of versions · open_deviation_count
count of deviations where status == 'open' · overdue_obligation_count count where status == 'overdue' ·
planned_amount sum of planned_amount · actual_amount sum of actual_amount. Read the
summaryOperations shape in @objectstack/spec/data before authoring; filtered roll-ups use the
filter predicate form the schema documents.

State machines: obligation pending→in_progress/done/waived/overdue; overdue→done/waived; in_progress→done/waived; payment plan planned→due→partial/paid/overdue; overdue→partial/paid.
overdue is entered only by the daily jobs (card 09) — a user write to overdue is refused.

Acceptance

  • Gates green; pnpm validate reports 11 objects.
  • Creating two versions on a contract reads back version_count == 2 through REST.

Out of scope

The jobs that flip overdue (09), dashboards (10).

Activity

  1. self-assigned this
    on Sep 7, 2026
  2. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01KcrVDXSptwDukFsHPHPR1V
    Branch: claude/issue-3-post-signature-domain
    Worktree: hotclm-issue-3
    Domain: repo:hotclm (single-lane repo)
    File surface: src/objects/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (mechanical — two objects and five roll-ups against a table already pinned in DESIGN.md §03)
    Clause-②: no
    Serial constraints cleared: Blocked-by: #2 is satisfied — PR #5 merged as f3f2a4e. Base is main @ ffff73b, which also carries PR #7 (the app is now signable-in, so this card can be browser-verified). #4 stays pm:blocked on this one; no other in-flight claim in this repo.


    Generated by Claude Code

  3. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 3,
      "status": "done",
      "branch": "claude/issue-3-post-signature-domain",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/9",
      "premise_still_valid": true,
      "summary": "Added clm_obligation and clm_payment_plan as controlled_by_parent master-detail children of clm_contract (cascade, nameField display_name, icons list-checks / banknote, amounts scale 2 min 0), the five Field.summary roll-ups on clm_contract, and the two child state machines plus their display_name mirrors beside card 02's in contract.hook.ts / mirror.hook.ts. Three commits, roll-ups on their own as the card suggested. All four Zone 2 assumptions held and were measured, not assumed: summaryOperations expresses both filtered counts via its documented filter form; the roll-ups silence the single rollup/missing-summary suggestion (1 suggestion on the base, 0 now); validate reports 11 objects, 169 fields; both machines stayed in contract.hook.ts, now 537 lines and still one coherent subject. Two authored choices beyond the card's literal text, both flagged in the PR body for cheap refusal: (contract, seq) is a unique index following clm_contract_version's precedent, and no roll-up declares max because a bound on an engine-written derived value would have the recompute refused and leave the roll-up silently stale. Zone 1's ASCII payment mirror was implemented; DESIGN.md §03's table still carries the localized spelling and was left alone as governed text. The assignee was already set by the dispatch and was not touched; the newest Claim on the card names this branch and this session.",
      "tests": "Gates via the shared verify lock, VERDICT command-exit 0: `pnpm validate` -> `✓ Validation passed (169ms)` / `Data: 11 Objects  169 Fields`; `pnpm lint` -> `✓ All checks passed (175ms)` (base measured first at `1 suggestion(s)` rollup/missing-summary on clm_contract_version.version_no); `pnpm typecheck` -> tsc --noEmit, no output, exit 0. `pnpm build` run once to prove the four new handlers lower rather than bundle: `Skipping legacy runtime bundle (all 12 callables are body-only)`. Control-character scan clean (grep -naP over src/objects). BROWSER: pnpm dev --seed-admin on OS_PORT=3103 detached; Playwright 1.63 against /opt/pw-browsers/chromium-1194/chrome-linux/chrome; signed in through the real form as admin@objectos.ai, landed on /_console/home, all calls made from inside that authenticated page. Boot read, not assumed: `✓ Server is ready`, 30 plugins, no degraded-capabilities line, no `no such table`, no failed plugin; HotCLM listed Enabled in Setup; /api/v1/meta/object returns all 11 clm_* objects. ROLL-UPS, parent re-read after each group, every read HTTP 200: planned_amount 0 -> 120000 on two instalments; actual_amount 0 -> 50000 when instalment 1 was paid; version_count 0 -> 1 -> 2 -> 1 across two version inserts and one delete (the card's acceptance criterion, and the delete leg proves it comes back down); open_deviation_count 0 -> 1 -> 0 as one deviation was moved into and back out of the filter predicate. That last measurement is the load-bearing one: a filtered count reading 0 is indistinguishable from a dead roll-up, so the predicate was exercised in both directions rather than being reported as a passing zero. overdue_obligation_count stayed 0 correctly, since no user session can reach that state at all. Readonly check: PATCH of {version_count: 99, planned_amount: 999999} returned 200 and moved neither stored value. STATE MACHINES over REST: allowed pending -> in_progress -> done, pending -> waived, planned -> due, due -> paid; refused with 422 + code INVALID_STATE on PATCH and POST to overdue on both objects, on done -> pending, and on planned -> paid; 409 UNIQUE_VIOLATION on a duplicate (contract, seq). Stamps read back: completed_at 2026-09-07T13:11:49.750Z on done, actual_date 2026-09-07 on paid. Mirrors as stored: 'Deliver implementation plan', '#1 · 2026-10-01', '#2 · 2027-01-01'. NOT MEASURED and why: no rendered list or record surface exists for these objects yet (/api/v1/meta/view returns 0 views, HotCLM contributes no app until card 05), so the human surface driven was the authenticated data API; and overdue_obligation_count cannot be moved from a user session by design, so it will first move under card 09's job. No ablation applies: this card adds no gate or guard whose failure mode needed proving by mutation, and the equivalent evidence (the filter moved in both directions, the readonly write attempted) was taken live instead.",
      "mcp_calls": "6 — search_issues x2 (one dedup query plus its control leg, after the repo-scoped REST read channel returned 403 and I declared the switch), issue_write x1, create_pull_request x1, pull_request_read x1 (full-body read-back of the published PR: byte-identical, Fixes #3 and the 验收备注 heading and the footer all survived the sanitizer), add_issue_comment x1 (this comment). Card body and comments were read through the zero-quota embedded-JSON web payload, not through MCP.",
      "open_questions": [
        {
          "question": "DESIGN.md §03 gives clm_obligation.in_progress no edge to overdue. Implemented literally, an obligation someone has started can never be marked in arrears — and those are the rows card 09's daily job will most often need to flip. Is the missing edge a ruling or an omission?",
          "options": [
            "A — omission: add in_progress -> overdue to §03 and to the machine, so the daily job can reach every late row",
            "B — ruling: an obligation actively being worked is reported through its due date, never moved into arrears; card 09 must skip in_progress rows, and that exclusion should be stated in §03 so it is not read as a bug later",
            "C — leave §03 silent and let card 09 decide at implementation time"
          ],
          "recommendation": "A. The asymmetry is hard to defend as intent: pending -> overdue is drawn, and 'started but late' is strictly a worse state than 'not started and late'. But this is DESIGN.md §01-§04, a governed surface, so it wants a needs-user-decision card rather than a dev's edit. C is the one option to avoid — it defers the same question to a card that will be under pressure to invent an answer quietly."
        },
        {
          "question": "DESIGN.md §03 gives clm_payment_plan.partial no outgoing edge at all. Implemented literally, a partially paid instalment can never be completed to paid. Terminal states are marked explicitly elsewhere in §03; here it is terminal only by omission.",
          "options": [
            "A — add partial -> paid (and probably partial -> overdue, for a part payment that then stalls)",
            "B — confirm partial is genuinely terminal, meaning the remainder is modelled as a further instalment row rather than as a status change on the same row",
            "C — leave as implemented and revisit when a card actually needs it"
          ],
          "recommendation": "A, or B stated explicitly — either is workable, but the current silence is not. B is coherent (a schedule of rows, each settled once) and fits the unique (contract, seq) shape, yet it needs saying, because 'partial' as a permanent resting state reads as a dead end to anyone using the product. Same governed-surface route as the question above."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8: every boot logs an ADR-0087 conversion warning — `required: true` implying NOT NULL is deprecated and retires from the load path in protocol 18, at which point 40 columns silently become nullable with no error to read. Pre-existing and repo-wide: 31 of the 40 sites predate this card (git grep -c on the base commit vs HEAD), and the runtime names card 01's clm_contract_type.name as the first converted site. AGENTS.md's own 'a boot that logs warnings is not a passing boot' is the standard it breaches.",
        "noted, not filed — needs the maintainer: the two missing state-machine edges above live in DESIGN.md §01-§04 and belong in needs-user-decision cards, not in a dev's diff.",
        "noted, not filed — needs the maintainer: DESIGN.md §03's field table still spells the payment-plan mirror in its localized form while this card's Zone 1 settled on ASCII with an explicit rationale. The code follows the card; the governed table was left untouched and now disagrees with it.",
        "noted, not filed: no storage HTTP endpoint is mounted — /api/v1/storage and /api/v1/storage/files both return 404 ENDPOINT_NOT_FOUND, so a file can only be attached by inserting a sys_file row by hand (which is how this run's version fixtures were built). clm_contract_version.file and clm_obligation.evidence are both file fields, so card 05's upload UI will meet this. Adding a capability token is a needs_decision per AGENTS.md, never a rider.",
        "noted, not filed: the Console logs `[AuthProvider] Failed to load organizations` from an aborted GET /api/v1/auth/organization/list on every sign-in, plus one 404 for a static asset. Pre-existing, unrelated to this card, and the Console renders and functions regardless."
      ]
    }

    Generated by Claude Code

  4. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — PM review, round 2

    Reviewed against GitHub. PR #9, head fc4a4ef.

    Item Verdict
    PR shape draft, base main, Fixes #3 ✓
    CI Validate green on fc4a4ef ✓
    File surface 6 files, all inside src/objects/ — no breach this round ✓
    Gates validate ✓ (11 objects, 169 fields) · lint ✓ (0 suggestions — the roll-ups silenced the one on the base) · typecheck ✓

    Spot checks I ran myself

    • Both child machines match DESIGN.md §03 literally, including the two edges it does not draw. Implementing the gap and asking about it — rather than quietly widening a governed surface — is the right call and the reason both questions below are answerable at all.
    • The roll-up rationale is sound. summary over formula is argued in the docblock on the grounds that a formula cannot be filtered or sorted on, which is exactly what the two filtered counts exist for. And the note on open_deviation_count — that the in_review → in_approval guard still reads the children directly because a guard must not trust a cached aggregate — is a correctness insight, not a comment.

    On the browser evidence. This is the first card under the new rule and it set the bar higher than the rule asked for:

    • The boot banner was read, not assumed: ready, 30 plugins, no degraded-capability line, no missing table, no failed plugin. That is the check AGENTS.md now demands, performed as written.
    • The roll-ups were driven in both directions. open_deviation_count 0 → 1 → 0 and version_count 0 → 1 → 2 → 1 (the last leg a delete). The stated reason is the load-bearing one: a filtered count reading 0 is indistinguishable from a dead roll-up, so the predicate was exercised both ways instead of a passing zero being reported as proof. A weaker run would have inserted two rows, seen 2, and called it verified.
    • The readonly claim was attacked, not asserted: a PATCH of {version_count: 99, planned_amount: 999999} returned 200 and moved neither stored value.
    • The scope statement is honest: no rendered view surface exists yet (0 views until card 05), so the human surface driven was the authenticated data API, and overdue_obligation_count cannot be moved from a user session by design — it first moves under card 09.

    Authored choices — both accepted. The unique (contract, seq) index follows clm_contract_version's precedent. Declining to put max on any roll-up is the better call and the reasoning is right: a bound on an engine-written derived value would have the recompute refused and leave the aggregate silently stale — a validation that makes the data less trustworthy.

    Open questions — both go to the maintainer, neither is mine. Both are DESIGN.md §03, a governed surface, and I confirmed both against the text rather than taking the report's word:

    - `clm_obligation.status`:pending→in_progress/done/waived/overdue;overdue→done/waived;in_progress→done/waived
    - `clm_payment_plan.status`:planned→due→partial/paid/overdue;overdue→partial/paid
    

    An obligation someone has started can never be marked late, and a partially paid instalment can never be completed. Filed together as decision card #10 with the analysis; card 09's daily jobs are the consumer and want an answer before they are dispatched.

    Findings. Issue #8 (the ADR-0087 required/notNull deprecation across 40 columns, silently nullable at protocol 18) is a real one and correctly separated: it is repo-wide, 31 of the 40 sites predate this card, and it breaches the very "a boot that logs warnings is not a passing boot" line that landed this morning. The unmounted storage endpoint is worth knowing before card 05 meets it with two file fields.

    Merging: CI green, ACCEPT recorded, no governed surface touched — the conditions AGENTS.md now sets for the loop to merge its own work.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions