Skip to content

The renewal-notice and expiration-sweep jobs produce nothing although 11 contracts carry is_expiring, and five of the six daily flows report acted: 0 while they notify - #102

Draft
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-97-renewal-expiry-jobs
Draft

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-97-renewal-expiry-jobs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #97

The run-summary acted: 0 on flows that did notify stays with the platform (objectstack-ai/objectstack#22590 item 2, per the card); nothing here touches or works around it.

Verdict: the two flows are correct, and the demo data gave them nothing to do

Measured on main @ 46e65f0 (17.7.0): fresh pnpm demo --port 3497, then POST /api/v1/automation/NAME/trigger for both jobs, signed in as the dev admin.

Job Node-level reading Why
renewal_notice (F12) notice_select 25 · gate Inside the window skipped 25 · notice_flag 0 runs The 25 rows the query reads are all outside their windows (the nearest by 6 days). The 11 contracts that ARE inside their window are seeded is_expiring = 1, and that flag is F12's once-per-contract key (is_expiring: { $ne: true }), so the query never returns them.
expiration_sweep (F13) expire_select 0 · autorenew_select 0 No active contract has an end date before today. The nearest ends in 4 days.

I applied DESIGN.md §06 F12 / F13 to all 60 seeded active contracts for the run date, computing from the seed plan rather than the database. F12's window holds exactly 11 contracts: the card's "11 contracts carry is_expiring", all seeded already flagged. F13's window holds 0. So the queries match §06, and the card's either/or comes out as "the demo's dates". PR #42 measured both write paths on probe rows on 17.4, and run 1 below measures them again on 17.7. The consequence: a fresh demo never sends a renewal reminder or an expiry reminder, which is half of M3's acceptance (DESIGN.md §11: 「到期、逾期提醒在收件箱可见」).

What changed: src/data/** only, no flow touched

The fix follows the pattern handBackInReview (F3) and leaveUnassigned (F10) already use: pick a row by a rule, rewrite it, then check the result again.

  • placeInSweepWindows in src/data/plan-contracts.ts, applied after handBackInReview:
    • F12. One backfilled active contract with a legal owner gets end_date set to renewal_notice_days days from boot, so its notice deadline is today: ICA-2026-0002, end moved from +81 to +60, notice 60 days. The rule only takes a notice period over 30 days and an end date already inside 90, so neither dated count below moves.
    • F13. Two backfilled active contracts with a legal owner get a term that ended yesterday, one for each branch: NDA-2026-0006 (does not auto-renew, so F13 expires it) and SUP-2026-0009 (auto-renews, so F13 drafts its renewal). Both originally ended more than 90 days out.
    • Each moved contract's whole timeline shifts by the same number of days, so start, signed and activated dates keep their distances from each other. No child row reads a contract's end date: payment and obligation dates are drawn independently.
    • Checks that run under pnpm validate: exactly 1 unflagged contract with a deadline of today; exactly 1 + 1 active contracts past their end date; §10's "10 within 30 days" is still 10; the 管理层 expiring_90_days band is still 12; no moved contract starts in the future.
  • renewalFlagged replaces isExpiring in contract.seed.ts and changes the predicate from endDate - renewalNoticeDays <= 0 to ... < 0. For F12 and F13, the fixture now describes the book as a daily run leaves it at the start of boot day: a deadline on an earlier day was flagged by that day's run, and a deadline of today is still waiting for today's run. So the seeded flag is the value F12 itself left (_shared.ts rule 2). For every row that already existed, the two predicates agree (the latest in-window deadline was day −3), so only the new row changes.
  • §10's status spread is unchanged at seed time: draft 10 · submitted 6 · in_review 12 · in_approval 8 · approved 4 · signing 6 · active 60 · expired 8 · terminated 4 · cancelled 2. is_expiring goes from 11 to 13, because both F13 rows were flagged back when their windows opened.

After: measured on 029c9ef

The later commits change only comments and error text. The seed output is byte-identical on both commits: the sha256 of JSON(CONTRACT_PLAN) is c58f14d9001b19be and the sha256 of the contract seed is 1eaf54465270b941, on 029c9ef and on 7d8a059.

Sequence: wipe .objectstack/data, pnpm demo --port 3497, do the README operator setup (create the five named accounts, grant clm_requester to the three requesters, give clm_legal_counsel to the two counsel), then run pnpm demo again. The handover reads owners 43 / 43 / 34 and legal owners 34 / 34. Before the first trigger: sys_inbox_message 0.

Run 1 (both triggers answered 200, success: true, failed: 0)

Job Node-level reading
renewal_notice notice_select 23 · notice_flag 1 run · notice_tell sent to 2 recipients
expiration_sweep expire_select 1 · expire_flag 1 · expire_tell to 2 · autorenew_select 1 · auto_existing 0 · auto_create 1 · auto_tell to 2

Database after the run, read with better-sqlite3 opened read-only:

contracts     active 60 -> 59   expired 8 -> 9   draft 10 -> 11   (all other statuses unchanged)
renewal draft SUP-2026-0019 "Renewal — Supplier Agreement — Pinnacle IT Hardware", draft,
              2026-10-10 .. 2029-10-09, renews SUP-2026-0009, owner Business Requester 1, legal Legal Counsel 1
inbox         0 -> 6   (sys_notification_receipt: 6 x inbox/delivered)
Recipient Topic Title
Business Requester 2 clm_renewal_notice_due Renewal decision due: Independent Contractor Agreement — Aisha Bello
Legal Counsel 2 clm_renewal_notice_due Renewal decision due: Independent Contractor Agreement — Aisha Bello
Business Requester 3 clm_contract_expired Expired: Mutual Non-Disclosure Agreement — Basalt Energy
Legal Counsel 2 clm_contract_expired Expired: Mutual Non-Disclosure Agreement — Basalt Energy
Business Requester 1 clm_contract_auto_renewed Renewal drafted: Supplier Agreement — Pinnacle IT Hardware
Legal Counsel 1 clm_contract_auto_renewed Renewal drafted: Supplier Agreement — Pinnacle IT Hardware

Run 2 (both jobs triggered again): renewal_notice notice_select 22, notice_flag 0 runs. expiration_sweep expire_select 0, autorenew_select 1, auto_existing 1, so no second draft. Inbox stays at 6 and renewal drafts stay at 1.

Browser: the right people see the reminders

Playwright with the pre-installed Chromium on http://localhost:3497/_console/. Two personas signed in through the login form.

  • Business Requester 2: inbox badge 1. The panel reads "Renewal decision due: Independent Contractor Agreement — Aisha Bello — Contract ICA-2026-0002 ends on 2026-12-09, and notice of non-renewal is owed by 2026-10-10 (60 days before). Start a renewal from the contract page, or give notice." Clicking it opens /_console/apps/app.objectstack.hotclm/clm_contract/record/4nhAxpFoBuKhTsYi, the ICA-2026-0002 page, with Start Renewal in the header, Business owner Business Requester 2 and Legal owner Legal Counsel 2.
  • Legal Counsel 2: inbox badge 2, showing "Expired: Mutual Non-Disclosure Agreement — Basalt Energy …" and the same renewal notice. Clicking the first message opens the NDA-2026-0006 record.
  • Browser console after sign-in: the only errors are GET /api/v1/data/sys_approval_request … -> 403 [PERMISSION_DENIED] on the contract record page, for both personas. This is already reported in the 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 pass (its finding 3) and is not touched here.

Trade-offs, measured

Proof the checks can fail (ablation, run from a commit)

node scripts/ablation-replace.mjs (the objectstack tool) mutated renewalFlagged back to <= 0 and ran pnpm validate under the shared verify lock. The anchor hit once, the blob changed bf1965a692ef -> 059271db73c2, and validate exited 1 with:

✗ demo fixture: 0 unflagged active contracts with a renewal deadline of today after placing the sweep-window rows, expected 1 — F12 would select no row inside its window and the demo would send no renewal notice (#97).

The restore was checked by the bytes, not the exit code: the blob equals HEAD (bf1965a692ef) and git diff HEAD is empty.

Gates, on the final head 7d8a059

pnpm verify (= the four CI steps) under the shared verify lock: VERDICT command-exit 0.

◆ Validate
  ✓ Validation passed (1239ms)
◆ Lint
  6 suggestion(s) (1005ms)
> hotclm@0.1.0 typecheck
> tsc --noEmit
✓ i18n gate
  LOCALES  : "en", "zh-CN" checked (required: en, zh-CN)
  COVERAGE : 0 missing keys across 2 locale(s)

The 7 validate warnings and 6 lint suggestions all name either the hierarchy-security capability or contract_approval nodes. This diff touches neither.

Acceptance notes

  • Scope. No flow file changed. The scheduled-work default stays off (Upgrade the @objectstack/* dependencies from 17.4.0 to 17.7.0, walking the official upgrade checklists of 17.5, 17.6 and 17.7 #82 is the decision on that); every run above was a manual trigger. No platform workaround.
  • Run-summary counters are not used as evidence. The summary's selected disagrees with the node-level count (for example, a summary of 25 against a notice_select of 23). That counter belongs to the platform, so every number above is node-level.
  • Observation, not filed: F11's seeded partial instalments. The 18 partial instalments planned 15 to 205 days ago are not the state a daily F11 would have left; they flip to overdue on the first run. That is why renewalFlagged claims the "start of boot day" reading for F12 and F13 only. Carrier: none.
  • Observation, not filed: the stage path on a backfilled contract. The contract page's path shows Draft through Signing as completed on a backfilled contract (ICA-2026-0002), which skipped review and approval by definition. This sits next to 34 backfilled contracts carry a legal_owner, and is_backfilled says on the same row that no review ever happened #53's open question about backfilled contracts. Carrier: none.

Generated by Claude Code

On a fresh demo `renewal_notice` acted on 0 of 25 and `expiration_sweep`
selected nothing: every active contract inside its notice window was seeded
already flagged `is_expiring` (F12's once-per-contract key), and no active
contract's term had ended. The jobs were correct and the renewal and expiry
reminders M3 asks to see in the inbox were never sent.

Read the fixture as the book at the start of boot day: a notice deadline on
an earlier day was flagged by that day's run, one of today is not yet. Move
one backfilled contract to a deadline of today (F12) and two to a term that
ended yesterday, one per F13 branch, and re-prove the selections and that
section 10's dated counts did not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
…y reading

The non-renewing F13 row, once expired, is refused on re-seed
(819 ok / 1 error) — state the measurement where the row is placed. The
"start of boot day" reading is claimed for F12 and F13 only; F11's seeded
partial instalments past their planned date are not read that way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
The placement counts are the fixture's own, not section 10's, so they are
refused with a message naming the job that would go quiet instead of the
"DESIGN.md section 10 pins" wording assertCount uses for the table's counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants