Skip to content

Review over SLA per contract type: a stored one-hop SLA offset on the contract, counted on the legal workbench - #201

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-31-review-sla
Nov 3, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-31-review-sla

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #31

Clause-②: no
Metadata-protocol: none
Ruling-ref: 6396322580 (letter C: keep the goal). The director's pointer on objectstack#26601 (2026-10-28T00:44Z) gives the platform-native answer: a stored expression field on clm_contract, read through contract_type, with the over-SLA filter on that column.

This PR delivers the per-type over-SLA half of DESIGN.md §09. The stage-durations half landed in PR #168. The legal workbench now counts the contracts in review whose review started longer ago than their own type's SLA. It replaces the fixed 30-day tile, and a new bar chart breaks the count down by contract type.

What changed

  • clm_contract.review_sla_days_offset (src/objects/contract.object.ts): a stored number field whose expression is 0 - record.contract_type.review_sla_days. The engine computes it on every insert and every update by id. A client-sent value never stands, and no hook, job or flow writes it. It is readonly, hidden, in the identity group with the other type-derived fields, and has scale: 0, min: -90, max: 0.
    • Why it is negative and not named review_sla_days, as the pointer spelled it. On 18.3 the $now anchor's addDays adds the column's value. There is no subDays and no negation (filter.zod.ts, the $now section). So "now minus the SLA" needs a column that holds the negative number. The platform documents this exact count with a column named review_sla_days_offset. A positive review_sla_days copy would compile to review_started_at < now + SLA, which counts every started review. The field name is a public contract, so the hand-back raises it as an open question.
  • contract_metrics.review_over_sla_count (src/datasets/contract.dataset.ts): a count with the measure filter status = in_review AND review_started_at < { $now: true, addDays: { $field: 'review_sla_days_offset' } }. Both widgets read this one definition (ADR-0021).
  • Legal workbench (src/dashboards/legal.dashboard.ts): review_ageing (审查超过 30 天) becomes review_over_sla (审查超时限) in the same slot. A new row 4 holds review_over_sla_by_type, a horizontal bar by contract_type_name with the largest count first. The 2B stand-in stood until the platform could express the comparison, and now it can. The stale comments in cycle-time.dataset.ts and the dashboard that said "not expressible" are corrected.
  • Translations (en + zh-CN): the field label and help in objects.contract.ts, plus the two widgets, the measure and the dashboard description in app.ts. The rebase onto 56ebd41 (A greyed lifecycle button only says 「对此记录不可用」 — 18.3 ships an authored disabledReason, so 送审 / 发起签署 / 生效 can say what is missing #200, which also adds keys to objects.contract.ts) was clean.

First: the one-hop stored expression, on one record (18.3.0, as requester1@)

Measured first with the pointer's exact spelling, a positive review_sla_days with record.contract_type.review_sla_days. Then measured again with the final field:

step review_sla_days (pointer spelling) review_sla_days_offset (final)
POST /api/v1/data/clm_contract, type NDA (SLA 3) 201, stored 3 201, stored -3
PATCH the draft's contract_type to Lease (SLA 10) 200, stored 10 (number re-issued LSE-2026-0007) 200, stored -10
PATCH {review_sla_days[_offset]: 99 / 5} 200, stored value unchanged (10) 200, unchanged (-10)
edit the type's own SLA NOT MEASURED: clm.admin@ got PATCH clm_contract_type → 403. By the platform's contract, existing contracts keep their value until each contract is next written (no reverse propagation) same

Verification: SQL vs API vs browser (zh-CN, as legal.head@example.com)

Fresh throwaway DB, pnpm demo:zh --port 3167 on 18.3.0. The independent figures are SQLite over .objectstack/data/objectstack.db: julianday(review_started_at) < julianday('now') - type.review_sla_days. The API figure is POST /api/v1/analytics/dataset/query as legal.head. The rendered figures are Chromium (/opt/pw-browsers/chromium, locale zh-CN) at /_console/apps/app.objectstack.hotclm/dashboard/legal_workbench.

type (SLA) in review SQL over SLA API rendered bar
主服务协议 (5) 7 6 6 6
双向保密协议 (3) 2 2 2 2
供应商协议 (5) 1 1 1 1
数据处理协议 (5) 1 1 1 1
租赁协议 (10) 1 1 1 1
独立承包人协议 (5) 1 1 1 1
审查超时限 tile 13 12 12 12
  • Under its SLA, absent. As legal.head, MSA-2026-0016 (submitted, MSA, SLA 5) was moved to in_review with counsel1 as legal owner. review_started_at was set to now and the offset stored as -5. 审查中 went 12 → 13 and the MSA row went 6 → 7 in review, while the over-SLA count stayed 12 (MSA 6).
  • Over its SLA, present, and invisible to the old tile. The 数据处理协议 contract has been in review 17.7 days against a 5-day SLA. It is counted here, but the old 30-day tile did not count it (its over30 was 0).
  • Dashboard filter. runtimeFilter: { category: 'nda' } gives 2, and SQL gives 2.
  • Before (origin/main fc68eb6, same persona, fresh DB): 待受理 6 · 审查中 12 · 审查超过 30 天 6 · 等待对方 2. The same four tiles after the change, before the probe: 6 · 12 · 审查超时限 12 · 2.
  • A type with nothing over its SLA is not drawn. Measured: selected alone, the measure filter narrows the rows (runtimeFilter: { review_started_at: { $gte: today } } gives rows: []). The chart's description says so.
  • Record page. The hidden field is not drawn on MSA-2026-0016's page (审查时限偏移 is absent from the page text).
  • Second pnpm demo:zh run, same DB. The boot is clean: Seeds: app.objectstack.hotclm 1130 rows, no degraded, no no such table, no failed plugin. The offset is NULL on 0 of 121 rows, and 0 rows disagree with -type.review_sla_days. The 24 author-time warnings in the boot banner are the same count as on the origin/main boot.
  • Browser console, before and after. The same 404 resource and the same pre-login 401 from GET /api/v1/auth/get-session appear on both reads, as PR Stage durations on the 18.2.0 duration measure; per-type over-SLA still blocked #168 recorded.

The browser readings were taken on fc68eb6 plus this change. The gates below ran on the rebased head. The rebase brought #199 (demo staff manager) and #200 (lifecycle disabledReason), and neither touches a dataset, a dashboard or this field.

Gates (head 4e58df8, each exit captured before any pipe)

pnpm install                  EXIT=0
pnpm validate                 EXIT=0   ✓ Validation passed (728ms)
pnpm lint                     EXIT=0   18 warning(s), 6 suggestion(s): 17 security-superuser-field-mask-inert (#196) + 1 obligation_requester_own (#64), the same as main
pnpm typecheck                EXIT=0
pnpm lint:i18n-gate           EXIT=0   COVERAGE : 0 missing keys across 2 locale(s)
CLM_DEMO_SEED=1 pnpm validate EXIT=0   ✓ Validation passed (802ms)

The field sits in identity and not lifecycle on purpose. CONTRACT_STAMPED_FIELDS derives the FLS stamp lock from the lifecycle / routing / ai groups. Measured: in lifecycle it added a 19th lint warning (a clm_admin mask beside modifyAllRecords, the #196 family). A stored expression needs no lock, because the engine replaces any value a client sends.

Acceptance notes

  • An existing database is not backfilled. Measured: booting this change over a DB created by origin/main left review_sla_days_offset NULL on all 121 rows. The column is added, and the platform computes a stored expression only when a row is written. A later demo run's seed rewrote 41 of 120 rows, which left 79 NULL. The $now anchor reads a NULL offset as 0 days (platform-documented), so an unbackfilled review counts as over SLA once it has started. The demo starts from a fresh DB and fills every row. 18.3 has no migrate step for stored expressions (os migrate has summary-nulls for roll-ups only). This goes to the seat as a platform finding.
  • Editing a type's SLA does not move contracts already filed. By the platform's contract, the stored value follows the contract's own writes, not the type's. A contract picks up the new SLA the next time it is written.
  • The F3 reminder (src/flows/legal-review-sla.flow.ts) and the help text of clm_contract_type.review_sla_days still describe a fixed 30-day reminder. Both are still accurate about the reminder, and both are outside this claim's surface. The flow could now read the same column.
  • §09's governed text is still the 2B wording ("审查超 30 天(固定阈值…)"). The seat carries the replacement on its design-doc PR Sync the governed design text with the rulings landed on 2026-10-25 (#96 #100 #11 #20 #38 #82) #120 (proposal in the os-dev-report on [Decision] §09 要「各段时长」和「超 SLA」,但语义层算不出时间差——两问共用一个补法 #31).

🤖 Generated with Claude Code

https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn

The legal workbench counted reviews older than a fixed 30 days because the
per-type SLA lives on clm_contract_type and the platform's $now anchor reads
its offset from the same record only (objectstack#25808, #26601). Platform
18.3 stores a one-hop expression on the contract, so clm_contract now carries
review_sla_days_offset, the type's review SLA as negative days computed by
the engine, and contract_metrics counts in-review contracts whose review
started before now plus that offset. The 30-day tile gives way to the
over-SLA count, with a per-type bar chart beside the stage durations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · head 4e58df8 · 2026-11-03T17:49Z

check reading
shape draft → main, Fixes #31 (the durations half already landed as PR #168); rebased on 56ebd41, mergeable clean
CI on the head both Validate check-runs success on 4e58df8 = PR head.sha
scope 8 files. clm_contract.review_sla_days_offset is a platform stored expression (0 - record.contract_type.review_sla_days), readonly, hidden, in the identity group; contract_metrics.review_over_sla_count counts in_review contracts whose review_started_at is before $now + offset; the legal workbench's fixed 30-day tile becomes 审查超时限, plus a per-type bar. No hook, job or flow writes the column. Lint 18 warnings, the same as main (placing the field in lifecycle would have added a 19th, measured)
measured (fresh pnpm demo:zh, signed in as legal.head) tile 12 = API 12 = independent SQL 12; per type MSA 6 · NDA 2 · SUP 1 · DPA 1 · LSE 1 · ICA 1 in all three. A review started now raises 审查中 12 → 13 and leaves 审查超时限 at 12. A DPA contract 17.7 days into a 5-day SLA is counted (the old 30-day tile missed it). A client-sent offset does not stand; re-picking the type recomputes it
seat call on the open question A — keep review_sla_days_offset: one hidden column holding the negative SLA. The $now anchor only adds, and this is the spelling the platform documents for this count. The governed text (§02 / §03 / §09 / §12) is on PR #120 (fa7f612), which the maintainer merges
residuals (1) a stored expression added to an object that already has rows is not backfilled, and a NULL offset reads as 0 days, so an upgraded (not fresh) database over-counts until each contract is next written. That is a platform gap; the seat files it on objectstack. (2) An edit of a type's own SLA does not reach contracts already filed until they are written (platform contract, noted). (3) The F3 reminder (legal-review-sla.flow.ts) still fires at a fixed 30 days; the seat files a hotclm card so the reminder and the tile agree

Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).


Generated by Claude Code

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.

[Decision] §09 要「各段时长」和「超 SLA」,但语义层算不出时间差——两问共用一个补法

2 participants