Skip to content

sys_activity.type declares 11 options while a shipped producer writes a 12th (scheduled) — readonly fields make the enum unenforceable #11424

Description

@claude

Filed from the objectui side while implementing objectstack-ai/objectui#5840 (record:activity was dropping every sys_activity.type: "scheduled" row). The renderer half is fixed there. This half lands here, because the declaration lives here.

Measured

packages/plugins/plugin-audit/src/objects/sys-activity.object.ts declares type as a select over eleven values:

created updated deleted commented mentioned shared assigned completed login logout system

scheduled is not among them. A shipped producer writes it anyway — objectstack-ai/hotcrm, src/actions/global.actions.ts:

constactivity=awaitctx.api.object('sys_activity').insert({type: EVENT_STATUS==='held' ? 'completed' : 'scheduled',
...

registered for crm_lead, crm_contact, crm_account, crm_opportunity and crm_case. Both branches land in the column; only the completed branch was declared.

Why the write is not rejected

This repo already measured the mechanism, in the vocabulary pin's own docblock: every field on sys_activity is readonly: true, and objectql's validateRecord skips readonly fields on both the insert and update branch, so the invalid_option check that enforces a select field's declared options never runs for one. The eleven-value enum is documentation, not a contract — an undeclared value is stored silently.

So this is not "an app broke the rules and got caught". Nothing can catch it. The row is written, stored and queryable under a value the platform never declared.

The general door, not the special case

completed's own census row records that the platform itself forwards author-declared values into this column: ADR-0052 section 5b.2 activityMilestones[].type, applied verbatim by audit-writers.ts (if (milestone.type) activityType = milestone.type). Any metadata author can put any string there and it lands. scheduled is simply the first one someone measured downstream.

That makes the question sharper than one missing enum member: is sys_activity.type a closed platform vocabulary or an author-extensible one? Both readings are currently true of the code, which is the actual defect.

What is at stake downstream

objectui's packages/plugin-detail/src/renderers/recordActivityFeed.ts keys its feed-kind map on this vocabulary. Its test used to pin the key set as set-equal to the eleven declared options. objectstack-ai/objectui#5840 breaks that set-equality on purpose — the map now covers the declaration PLUS the values a shipped producer measurably writes — because pinning to the declaration means dropping stored rows. Whichever way this is ruled, that pin should follow it.

Directions

  1. Declare it — add scheduled to the enum and a TYPES_WITH_WRITERS row naming the HotCRM writer. Cheapest, and makes the declaration match reality. Does not address the general door.
  2. Rule the producer non-conformant — HotCRM must use a declared value. Keeps the vocabulary closed, but nothing enforces it, so it is a convention that will be broken again silently.
  3. Enforce the vocabulary — make readonly select fields validate their options on system-owned writes, so an undeclared value is rejected loudly at the point of writing. Largest, and the only one that stops the class.
  4. Declare it author-extensible — accept that milestone-declared types pass through, and say so, which makes downstream closed maps the bug.

Recommending 1 as the immediate step (it is reversible and it makes the shipped state honest) with 3 as the direction that actually closes it — but this is a vocabulary ruling and a maintainer call, not an objectui one. Recorded, not acted on.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions