Skip to content

feat(showcase): make v16 approvals demonstrable out of the box#3364

Merged
os-zhuang merged 2 commits into
mainfrom
feat/showcase-approval-demo
Jul 21, 2026
Merged

feat(showcase): make v16 approvals demonstrable out of the box#3364
os-zhuang merged 2 commits into
mainfrom
feat/showcase-approval-demo

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Why

During the #3358 v16.0 verification sweep, the marquee approval features weren't demonstrable in the showcase out of the box — the checklist assumes "seeds make most features demonstrable," but for §1 they didn't. Root causes:

  1. Dangling positions (a real showcase bug): the showcase_invoice_signoff and showcase_committee_quorum flows route to finance / legal positions that were never defined (positions.ts only had manager/exec/auditor/ops/…). So the flows were unroutable even if triggered manually.
  2. No approver holders: position assignments are runtime admin actions and users can't be seeded → every request resolved to an empty approver slate and waited forever.
  3. No opened request: the seed loader suppresses record-change flows (fix(seed/automation): package seed can't wedge the platform — suppress flows on seed + coerce SQLite booleans + re-entrancy loop guard #2661), and sys_approval_request is engine-owned (ADR-0103, get/list only), so nothing seeded a pending request.

What

Mirrors the existing bind-position-sets.ts pattern (imperative, on kernel:bootstrapped):

  • Define finance + legal positions (positions.ts, now 9 positions).
  • seed-approval-demo.ts (new): assigns the dev admin to manager/finance/legal (sys_user_position), provisions a phone-based demo user, and launches both signoff flows through the real automation engine so two genuine, resumable pending requests land in the inbox on first boot.
    • Org is resolved from sys_member (the admin's sys_user.organization_id is null) and threaded into both the position rows and the requests — otherwise the org-scoped approver resolution and getRequest (inbox drawer) silently return nothing.
    • The engine's execute() evaluates the start-node condition and reads the target object from context.object, so the launch supplies previous (for the previous.status != "sent" gate) and object + organizationId.
  • Seed a high-value EXP-DEMO report ($8,900, submitted) for the 2-of-3 quorum flow.
  • "Submit for Sign-off" invoice action so the 会签 flow can be re-triggered from the UI on demand.

Idempotent throughout (persistent DB keeps rows; duplicate-pending requests are rejected and swallowed). No changeset — @objectstack/example-showcase is private.

Verified

Fresh objectstack dev --ui --seed-admin:

  • Approval Center shows 2 pending requests (待我审批 = 2): High-Value Expense — Committee Quorum (2 of 3) and Invoice Dual Sign-off (finance + legal).
  • The detail drawer renders server-computed progress ("Approvals — 0 of 1" — the 2→1 quorum clamp when all slots resolve to the one admin, i.e. the "thresholds clamp so misconfiguration can't deadlock" behavior) and the metadata-driven action bar (Approve / Reject / Reassign / Send back / Request info).
  • ?request=<id> deep-links open the drawer directly.
  • os validate ✓ (9 Positions, 8 Actions) and tsc --noEmit ✓.

Known limitation

Because users can't be seeded, the admin holds all three approver positions, so the three approver slots all resolve to "Dev Admin" and the quorum collapses to 1 distinct approver. The decision mechanics (quorum tally, progress, actions, deep links) are fully exercised; a multi-user deployment would show distinct approvers.

🤖 Generated with Claude Code

The v16 marquee approval features (M-of-N quorum + finance∧legal 会签,
server-computed progress, metadata-driven decision actions, ?request= deep
links) could not be exercised in the showcase on a fresh boot:

- the approval flows route to `finance`/`legal` positions that were never
  defined (only manager/exec/... existed) — a dangling reference that made
  the flows unroutable even when triggered manually;
- no user held any approver position (assignments are runtime admin actions,
  users can't be seeded), so every request resolved to an empty slate;
- the seed loader suppresses record-change flows (#2661), so seeding a `sent`
  invoice never opened a request; and `sys_approval_request` is engine-owned
  (ADR-0103), so a request can't be inserted through the data API either.

Fix, mirroring `bind-position-sets.ts` (imperative, on `kernel:bootstrapped`):

- define the `finance` + `legal` approval-routing positions;
- assign the dev-seeded admin to manager/finance/legal (`sys_user_position`)
  so they resolve as an approver and can act in the inbox — org resolved from
  `sys_member` (the admin's `sys_user.organization_id` is null), so the
  org-scoped approver resolution + `getRequest` both match;
- provision a phone-based demo user so the "phone sign-in surfaces" show a real
  number in the All Users list + detail;
- seed a high-value (`$8,900`) submitted `EXP-DEMO` report and launch the
  Invoice Dual Sign-off (会签) and High-Value Committee Quorum (2-of-3) flows
  through the real automation engine, so two genuine, resumable pending
  requests land in the inbox on first boot;
- add a "Submit for Sign-off" record action on invoices so the flow can be
  re-triggered on demand from the UI.

Idempotent throughout (persistent DB keeps the rows; `openNodeRequest` rejects
duplicate pending requests, which we swallow). Verified: on a fresh
`objectstack dev --seed-admin`, the Approval Center shows both requests
(待我审批 = 2) with the server-computed progress ("Approvals — 0 of 1", the
2→1 quorum clamp) and the metadata-driven Approve/Reject/Reassign/Send-back/
Request-info bar. `os validate` + `tsc --noEmit` pass.

Found during the #3358 v16.0 verification sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 21, 2026 2:23am

Request Review

The v16 approval-demo change added `finance` + `legal` positions to
positions.ts (7 → 9), but seed.test.ts still asserted 7, failing Test Core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the tests label Jul 21, 2026
@os-zhuang
os-zhuang merged commit ffb83ae into main Jul 21, 2026
15 checks passed
@os-zhuang
os-zhuang deleted the feat/showcase-approval-demo branch July 21, 2026 03:08
os-zhuang added a commit that referenced this pull request Jul 21, 2026
…ation (#3393)

The v16 sweep (#3358 §4) couldn't exercise three shipped features because the
showcase never demonstrated them out of the box — a gap `coverage.ts` doesn't
catch (it tracks metadata KINDS, not sub-features like action-param widget
types or list pagination). Fill them:

- **Action-param widget gallery** (`ActionParamGalleryAction` on Field Zoo): one
  inline param of every non-trivial type so the ADR-0059 `ActionParamDialog`
  renders each real field widget — richtext editor, color picker, date picker,
  select, number, the AutoNumber widget for an `autonumber` param, and the ⚠️
  `image`/`file` uploads (multiple/accept/maxSize + the upload guard). No
  showcase action declared `params` before, so these dialogs were undemonstrated.
- **Related-list pagination**: 24 bulk prospects under one account (Northwind)
  so its Account → Contacts related list exceeds a page and demonstrates
  server-side `$top`/`$skip` windowing (objectui#2711) on a fresh boot.
- Record the sub-feature demonstration in the `action` coverage entry.

Verified in the running app: the ParamDialog renders richtext/select/date/color/
autonumber widgets + image/file upload zones (not text boxes); the Northwind
Contacts related list holds 26 rows but fetches `?top=5` (windowed, not
load-all). `os validate` (9 Actions) + `tsc --noEmit` pass.

Follow-up to #3364, from the #3358 sweep §4.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 22, 2026
The v16 sweep (#3358 §4 "Record History tab") couldn't exercise the tab because
no showcase object opted into it: the console gates the History tab on
object-level `enable.trackHistory` (RecordDetailView) — audit *capture* is
always on, but the *tab* is opt-in — and the showcase only set `trackHistory`
at the FIELD level (industry/status), which just selects which diffs are
summarized. So the tab never rendered anywhere and #2691/#3293 (display-value
diffs, computed-field exclusion) were undemonstrated.

Set `enable: { trackHistory: true }` on Account (which already declares
field-level `trackHistory` on `industry`/`status` for clean diffs).

Verified in the running app: the Account detail now shows a **历史 / History**
tab; after editing `industry`, it renders "Dev Admin · UPDATE · 行业: Retail →
Technology" — field label + select **display values** (not raw stored codes/ids),
no phantom value→null rows. `os validate` + `tsc --noEmit` pass.

Follow-up to #3364 / #3393, from the #3358 sweep §4.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 22, 2026
…e phone demo user (#3408)

The approval demo's `ensurePhoneDemoUser` inserted `sys_user` with an
`organization_id` key. `sys_user` has no such field — not in the object
definition (25 fields, none org/tenant) and not physically (26 columns, no
`organization_id`); org membership lives on `sys_member`. The key is not
dropped on the way down: it reaches SQL as a real column and the insert dies
with `table sys_user has no column named organization_id`.

The insert is wrapped in a best-effort try/catch, so this surfaced only as a
warn line and an ERROR in the boot log — and the demo user was **never**
provisioned on any boot, leaving the phone surfaces (#3358 §6 "Phone sign-in
surfaces": All Users list, record detail) empty with nothing to look at.

Also drop the dead `admin.organization_id` read in `run`: the same missing
field made it permanently `undefined`, so only the `sys_member` fallback ever
ran. Resolve from `sys_member` directly and correct the comment, which claimed
the column existed but was null.

Verified on a wiped dev DB: boot log goes from 1 ERROR to 0, and
`sys_user` now holds `usr_showcase_phone_demo` / `Mei Phone (demo)` /
`+8613800138000`. Approval fixtures unaffected (2 pending requests + 3 admin
positions still stamped with the org). `tsc --noEmit` passes.

Follow-up to #3364; found while collecting evidence for #3358 §1.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant