Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/publish-sys-activity-builtin-vocab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': minor
'@objectstack/plugin-audit': patch
---

Publish the built-in `sys_activity.type` vocabulary from `@objectstack/spec` (#11807).

`SYS_ACTIVITY_BUILTIN_TYPES` (and the derived `SysActivityBuiltinType` union) is now exported from `@objectstack/spec/data` (`feed.zod.ts`, alongside `FeedItemType`), and the `sys_activity` object declaration in `@objectstack/plugin-audit` derives its `type` options from it — one source instead of a hand-copied list per consumer.

The constant is the platform's **built-in set, not the column's value domain**: `sys_activity.type` stays an open, author-extensible vocabulary (#11507 ruling — an app may contribute values via `activityMilestones[].type`, ADR-0052 §5b.2, or its own inserts, and undeclared values are stored verbatim). It is deliberately a plain `as const` tuple rather than a `z.enum`, so it cannot be used as a validator; consumers must render unknown values, never drop them. UI packages that hand-copied the list (objectui's feed-kind census, which drifted the day #11522 added `scheduled`) can now read this export instead.
13 changes: 8 additions & 5 deletions content/docs/references/data/feed.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ description: Feed protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Activity-timeline UI config enums.
Activity-timeline UI config enums, and the `sys_activity.type` built-in set.

The `service-feed` backend was retired (ADR-0052 §5 / #1955); `sys_comment` /
`sys_activity` are the canonical record-collaboration/timeline backend. Only these
two enums remain here — they are pure UI configuration for the record activity
component (`RecordActivityProps` in `../ui/component.zod.ts`), with no backend
dependency. (A later `feed` → `activity` rename is tracked separately.)
`sys_activity` are the canonical record-collaboration/timeline backend. The two
enums here are pure UI configuration for the record activity component
(`RecordActivityProps` in `../ui/component.zod.ts`), with no backend dependency;
`SYS_ACTIVITY_BUILTIN_TYPES` below is the published built-in vocabulary of the
`sys_activity.type` column, co-located with `FeedItemType` because UI consumers
map one onto the other. (A later `feed` → `activity` rename is tracked
separately.)

<Callout type="info">
**Source:** `packages/spec/src/data/feed.zod.ts`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import { SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/spec/data';
import { SysActivity } from './index.js';

/**
Expand Down Expand Up @@ -92,6 +93,21 @@ import { SysActivity } from './index.js';
* that map has since been widened past the declaration to cover values a shipped
* producer measurably writes, so the two key sets are no longer set-equal —
* recorded from that card, not measured here, for the same reason.
*
* ## 2026-08-25 — #11807: the built-in set is now PUBLISHED, and this census
* pins the spec constant transitively
*
* The built-in set moved to `SYS_ACTIVITY_BUILTIN_TYPES` in
* `@objectstack/spec/data` (feed.zod.ts), and the object declaration spreads it
* — one source, readable by the UI packages whose hand-copy drifted (#11522's
* `scheduled`). Two consequences here:
*
* - The literal census below now transitively pins the SPEC constant: editing
* the published set without redoing the census goes red in this file. That
* is the intended cost — a built-in-set edit is a contract change.
* - A new wiring pin asserts the object's declared options ARE the published
* set, so re-inlining a diverging list in the object (the pre-#11807 shape)
* cannot come back silently.
*/

/**
Expand Down Expand Up @@ -208,6 +224,26 @@ describe('sys_activity — the `type` vocabulary is pinned to a writer census (#
).toEqual(inventoried);
});

/**
* [#11807] The single-source wiring pin. The object spreads
* `SYS_ACTIVITY_BUILTIN_TYPES` today, which makes this assertion look
* tautological — it is not: it goes red the day someone re-inlines a literal
* option list in `sys-activity.object.ts` that diverges from the published
* set, which is exactly the hand-copy shape whose drift #11807 retired. With
* this pin plus the census above, the chain is closed in both directions:
* spec constant === object declaration === writer census.
*/
it('declares exactly the published built-in set — object and spec cannot diverge (#11807)', () => {
expect(
[...typeValues()].sort(),
'sys_activity.type\'s declared options no longer equal SYS_ACTIVITY_BUILTIN_TYPES '
+ '(@objectstack/spec/data, feed.zod.ts). The published constant is the single '
+ 'source of the built-in set (#11807): declare the options by spreading it, and '
+ 'change the set only in the spec (with a changeset + a census redo here), never '
+ 'by re-inlining a list in the object.',
).toEqual([...SYS_ACTIVITY_BUILTIN_TYPES].sort());
});

/**
* The #8147 `import` lesson as a standing assertion: a value with a KNOWN
* writer must not leave the enum. Redundant with the set equality above only
Expand Down
27 changes: 12 additions & 15 deletions packages/plugins/plugin-audit/src/objects/sys-activity.object.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { ObjectSchema, Field } from '@objectstack/spec/data';
import { ObjectSchema, Field, SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/spec/data';

/**
* sys_activity — Lightweight Activity Stream
Expand Down Expand Up @@ -97,22 +97,19 @@ export const SysActivity = ObjectSchema.create({
* - Downstream, every CLOSED map over this vocabulary is now the bug: a
* consumer must render an unknown value, not drop the row. (objectui's
* feed-kind map is the known one; its pin is a card in that lane.)
*
* ## Where the built-in set lives (#11807)
*
* The options below spread `SYS_ACTIVITY_BUILTIN_TYPES` from
* `@objectstack/spec/data` (`feed.zod.ts`) — the PUBLISHED single source,
* readable by UI packages that cannot depend on this plugin. Do not inline
* the list again here: hand-copies are how objectui's census drifted the
* day #11522 added `scheduled`. Growing or shrinking the set is a spec
* change (with a changeset) plus a census redo in
* `sys-activity-type-vocabulary.test.ts`, in the same PR.
*/
type: Field.select(
[
'created',
'updated',
'deleted',
'commented',
'mentioned',
'shared',
'assigned',
'completed',
'scheduled',
'login',
'logout',
'system',
],
[...SYS_ACTIVITY_BUILTIN_TYPES],
{
label: 'Type',
description:
Expand Down
2 changes: 2 additions & 0 deletions packages/spec/api-surface/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@
"STACK_RUNTIME_MEMBERS (const)",
"STRING_VALUE_TYPES (const)",
"STRUCTURED_JSON_TYPES (const)",
"SYS_ACTIVITY_BUILTIN_TYPES (const)",
"Scalar (type)",
"SchemaMode (type)",
"SchemaModeSchema (const)",
Expand Down Expand Up @@ -594,6 +595,7 @@
"StateMachineValidationParsed (type)",
"StateMachineValidationSchema (const)",
"StringOperatorSchema (const)",
"SysActivityBuiltinType (type)",
"TEMPORAL_CASES (const)",
"TEMPORAL_NOW (const)",
"TEMPORAL_ROWS (const)",
Expand Down
2 changes: 2 additions & 0 deletions packages/spec/export-origins/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@
"STACK_RUNTIME_MEMBERS": "src/data/authoring-key-lint.ts#STACK_RUNTIME_MEMBERS (const)",
"STRING_VALUE_TYPES": "src/data/field-value.zod.ts#STRING_VALUE_TYPES (const)",
"STRUCTURED_JSON_TYPES": "src/data/field-value.zod.ts#STRUCTURED_JSON_TYPES (const)",
"SYS_ACTIVITY_BUILTIN_TYPES": "src/data/feed.zod.ts#SYS_ACTIVITY_BUILTIN_TYPES (const)",
"Scalar": "src/data/filter.zod.ts#Scalar (type)",
"SchemaMode": "src/data/datasource.zod.ts#SchemaMode (type)",
"SchemaModeSchema": "src/data/datasource.zod.ts#SchemaModeSchema (const)",
Expand Down Expand Up @@ -594,6 +595,7 @@
"StateMachineValidationParsed": "src/data/validation.zod.ts#StateMachineValidationParsed (type)",
"StateMachineValidationSchema": "src/data/validation.zod.ts#StateMachineValidationSchema (const)",
"StringOperatorSchema": "src/data/filter.zod.ts#StringOperatorSchema (const)",
"SysActivityBuiltinType": "src/data/feed.zod.ts#SysActivityBuiltinType (type)",
"TEMPORAL_CASES": "src/data/temporal-conformance.ts#TEMPORAL_CASES (const)",
"TEMPORAL_NOW": "src/data/temporal-conformance.ts#TEMPORAL_NOW (const)",
"TEMPORAL_ROWS": "src/data/temporal-conformance.ts#TEMPORAL_ROWS (const)",
Expand Down
41 changes: 40 additions & 1 deletion packages/spec/src/data/feed.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { FeedItemType, FeedFilterMode } from './feed.zod';
import { FeedItemType, FeedFilterMode, SYS_ACTIVITY_BUILTIN_TYPES } from './feed.zod';

describe('FeedItemType', () => {
it('should accept all valid feed item types', () => {
Expand All @@ -19,6 +19,45 @@ describe('FeedItemType', () => {
});
});

describe('SYS_ACTIVITY_BUILTIN_TYPES (#11807)', () => {
/**
* Mechanical invariants only. The semantic pin — every entry has a recorded
* writer disposition — lives with the object that declares the column:
* `plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts` (the writer
* census). That census reads the object's declared options, and the object
* derives them from this constant, so an edit here without a census redo goes
* red THERE. Duplicating the census literal in this package would create a
* second inventory that can drift from the first — exactly what #11807 exists
* to end.
*/
it('is a non-empty set of unique snake_case machine names', () => {
expect(SYS_ACTIVITY_BUILTIN_TYPES.length).toBeGreaterThan(0);
expect(new Set(SYS_ACTIVITY_BUILTIN_TYPES).size).toBe(SYS_ACTIVITY_BUILTIN_TYPES.length);
for (const type of SYS_ACTIVITY_BUILTIN_TYPES) {
// Machine-name convention (Prime Directive #3), and also what guarantees
// Field.select()'s snake_case option normalization is an identity when
// the sys_activity declaration spreads this constant into its options.
expect(type).toMatch(/^[a-z][a-z0-9_]*$/);
}
});

/**
* The vocabulary is OPEN (#11507): the published shape must not be able to
* reject anything. A plain readonly tuple has no parse/validate affordance;
* a z.enum here would read as a value-domain validator and re-close the
* vocabulary the day someone calls .parse() with it.
*/
it('is a plain tuple, not a Zod schema — the open vocabulary must not gain a validator', () => {
expect(Array.isArray(SYS_ACTIVITY_BUILTIN_TYPES)).toBe(true);
expect(
(SYS_ACTIVITY_BUILTIN_TYPES as unknown as { parse?: unknown }).parse,
).toBeUndefined();
expect(
(SYS_ACTIVITY_BUILTIN_TYPES as unknown as { safeParse?: unknown }).safeParse,
).toBeUndefined();
});
});

describe('FeedFilterMode', () => {
it('should accept valid filter modes', () => {
['all', 'comments_only', 'changes_only', 'tasks_only'].forEach(mode => {
Expand Down
71 changes: 66 additions & 5 deletions packages/spec/src/data/feed.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import { z } from 'zod';

/**
* Activity-timeline UI config enums.
* Activity-timeline UI config enums, and the `sys_activity.type` built-in set.
*
* The `service-feed` backend was retired (ADR-0052 §5 / #1955); `sys_comment` /
* `sys_activity` are the canonical record-collaboration/timeline backend. Only these
* two enums remain here — they are pure UI configuration for the record activity
* component (`RecordActivityProps` in `../ui/component.zod.ts`), with no backend
* dependency. (A later `feed` → `activity` rename is tracked separately.)
* `sys_activity` are the canonical record-collaboration/timeline backend. The two
* enums here are pure UI configuration for the record activity component
* (`RecordActivityProps` in `../ui/component.zod.ts`), with no backend dependency;
* `SYS_ACTIVITY_BUILTIN_TYPES` below is the published built-in vocabulary of the
* `sys_activity.type` column, co-located with `FeedItemType` because UI consumers
* map one onto the other. (A later `feed` → `activity` rename is tracked
* separately.)
*/

/**
Expand Down Expand Up @@ -45,3 +48,61 @@ export const FeedFilterMode = z.enum([
'tasks_only',
]);
export type FeedFilterMode = z.input<typeof FeedFilterMode>;

/**
* The platform's BUILT-IN set of `sys_activity.type` values — the single source
* the `sys_activity` object declaration (`@objectstack/plugin-audit`) and UI
* consumers read, so the built-in vocabulary can no longer drift between them
* (#11807: objectui's hand-copied census missed `scheduled` on the very day
* #11522 declared it).
*
* ## Built-in set, NOT the column's value domain
*
* `sys_activity.type` is an OPEN, author-extensible vocabulary (maintainer
* ruling 2026-08-24, #11507): this list is the floor the platform itself writes
* and offers in pickers/filters, never the ceiling of legal values. An app may
* contribute its own values — the sanctioned authoring channel is
* `activityMilestones[].type` (ADR-0052 §5b.2, `z.string()`, forwarded
* verbatim), and an app's server-side action can insert one directly — and an
* undeclared value is stored verbatim, not rejected.
*
* What that binds for a consumer of this constant:
*
* - ⛔ Never use it to validate, reject, or filter OUT values. A row whose
* `type` is not in this set is legitimate; render it (generic fallback), do
* not drop it. Every CLOSED map over this vocabulary is a bug (#11507).
* - It is deliberately a plain `as const` tuple rather than a `z.enum`: a Zod
* schema here would read as a validator and quietly re-close the vocabulary.
* - This is a different vocabulary from {@link FeedItemType}: `FeedItemType` is
* the UI timeline item-type enum; UI code MAPS `sys_activity.type` onto it
* and must map unknown values to a fallback, not drop them.
*
* Adding or removing an entry is a contract change on a published surface (that
* is the point — drift becomes a red gate instead of a rendering bug): it must
* ship with a changeset, and the writer census in
* `plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts` must be
* re-done in the same PR (that test goes red until it is).
*/
export const SYS_ACTIVITY_BUILTIN_TYPES = [
'created',
'updated',
'deleted',
'commented',
'mentioned',
'shared',
'assigned',
'completed',
'scheduled',
'login',
'logout',
'system',
] as const;

/**
* One built-in `sys_activity.type` value — derived from
* {@link SYS_ACTIVITY_BUILTIN_TYPES}. The column's runtime value domain is
* wider (`string`): the vocabulary is open and author-extensible (#11507), so
* code that READS rows must type the column as `string` and treat this union as
* the known-built-in narrowing only.
*/
export type SysActivityBuiltinType = (typeof SYS_ACTIVITY_BUILTIN_TYPES)[number];
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol
- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/feed.zod.ts` — Activity-timeline UI config enums.
- `node_modules/@objectstack/spec/src/data/feed.zod.ts` — Activity-timeline UI config enums, and the `sys_activity.type` built-in set.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
Expand Down
Loading