Skip to content

Commit 038ccde

Browse files
os-zhuangclaude
andauthored
feat(audit): sys_activity source pointer — the ActivityPointer model (ADR-0052 §5) (#1957)
Add `source_object` / `source_id` to `sys_activity`. `object_name`/`record_id` say WHICH record an activity is about (the "regarding" record); the new pair points to the RICH ENTITY it was derived from — the email row in `sys_email`, the call/meeting task, the `sys_comment` — so the timeline drills from a one-line summary to the full record, and apps can query "all activities sourced from X". This completes `sys_activity` as a proper ActivityPointer base (cf. Dataverse ActivityPointer → Email/PhoneCall/Appointment; Salesforce ActivityTimeline → EmailMessage/Task/Event): one materialized, indexed timeline that references — never duplicates — the rich, separately-tabled communication entities. The structured, queryable replacement for an id buried in `metadata`. Deliberately NOT done (per the design review): extending the `type` enum with domain verbs (email/call/meeting) — that couples a shared platform primitive to one vertical's vocabulary. `type` stays neutral; domain kind rides in `metadata.kind`; the rich entity lives in its own table. - sys-activity.object.ts: + source_object, source_id (text, searchable, optional) - ADR-0052 §5 updated to the ActivityPointer model - Verified in the showcase: a source-pointed activity round-trips and is queryable by source_object; plugin-audit 18/18 tests pass. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2daa8e2 commit 038ccde

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

docs/adr/0052-audit-is-not-the-activity-feed.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,31 @@ timeline. But weighing it against the implementation reality reversed that lean:
178178
| threads/mentions/reactions | ✅ fields already declared (`parent_id`, `reply_count`, `mentions`, `reactions`) | ✅ (but unreachable) |
179179

180180
Picking the durable, default, UI-wired system reaches "one backend" **now**, at
181-
near-zero risk. `service-feed`'s only real edge — one unified *typed* stream —
182-
is obtained on the chosen family by treating **`sys_activity` as the unified
183-
typed timeline** (its `type` enum already carries the event kinds; extend it to
184-
`email | call | event | note` as needed). The two remaining UI niceties
185-
(reactions, threaded replies) are a render of fields `sys_comment` **already**
186-
has — an objectui enhancement, not a backend change.
181+
near-zero risk. `service-feed`'s only real edge — one unified *typed* stream — is
182+
obtained on the chosen family by treating **`sys_activity` as the unified
183+
timeline base** — the **ActivityPointer** model (cf. Dataverse `ActivityPointer`
184+
`Email`/`PhoneCall`/`Appointment` subtypes; Salesforce ActivityTimeline →
185+
`EmailMessage`/`Task`/`Event`):
186+
187+
- **`type` stays domain-NEUTRAL** — the platform-produced verbs (`created`,
188+
`updated`, `commented`, `completed`, …). It is **not** extended with one
189+
vertical's vocabulary (`email`/`call`/`meeting`); every domain has its own
190+
(`interview`, `site_visit`, `inspection`, …) and a closed enum would be an
191+
endless treadmill. Domain kind rides in `metadata.kind`.
192+
- **Rich communication entities are their own tables** — an email belongs in
193+
`sys_email` (already exists), a call/meeting in a task/activity object — never
194+
crammed into a generic activity blob (they have structured headers, threading,
195+
attachments, mutable delivery status that must be queryable).
196+
- **`sys_activity` carries a structured pointer to that source entity** via
197+
`source_object` / `source_id` (added in this PR) — distinct from
198+
`object_name`/`record_id` (the *regarding* record). The timeline drills from a
199+
one-line summary to the full email/call record, and apps can query "all
200+
activities sourced from `sys_email`". This is the queryable equivalent of an id
201+
buried in `metadata`.
202+
203+
The two remaining UI niceties (reactions, threaded replies) are a render of
204+
fields `sys_comment` **already** has — an objectui enhancement, not a backend
205+
change.
187206

188207
Rejected alternative — invest in `service-feed`: building a DB adapter + mounting
189208
the REST route + repointing ChatterPanel + migrating `sys_comment` rows is weeks

packages/plugins/plugin-audit/src/objects/sys-activity.object.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ export const SysActivity = ObjectSchema.create({
128128
group: 'Target',
129129
}),
130130

131+
// ── Source pointer (ADR-0052 §5 — ActivityPointer model) ─────────
132+
// `object_name`/`record_id` say WHICH record this activity belongs to (the
133+
// "regarding" record, e.g. the contact). `source_object`/`source_id` point
134+
// to the RICH ENTITY this activity was derived from — the email row in
135+
// `sys_email`, the call/meeting in a task object, the `sys_comment` — so the
136+
// timeline can drill from a one-line summary to the full record. This is the
137+
// queryable, structured equivalent of cramming an id into `metadata`
138+
// (cf. Dataverse ActivityPointer → Email/PhoneCall/Appointment subtypes,
139+
// Salesforce ActivityTimeline → EmailMessage/Task/Event). Optional: most
140+
// CRUD activities have no distinct source (the record IS the source).
141+
source_object: Field.text({
142+
label: 'Source Object',
143+
required: false,
144+
readonly: true,
145+
searchable: true,
146+
maxLength: 255,
147+
description: 'Object name of the rich source entity this activity was derived from (e.g. "sys_email"). Null when the activity is about the target record itself.',
148+
group: 'Target',
149+
}),
150+
151+
source_id: Field.text({
152+
label: 'Source ID',
153+
required: false,
154+
readonly: true,
155+
searchable: true,
156+
maxLength: 255,
157+
description: 'Record id of the rich source entity (paired with source_object) — lets the timeline drill to the full email/call/meeting record.',
158+
group: 'Target',
159+
}),
160+
131161
url: Field.url({
132162
label: 'URL',
133163
required: false,

0 commit comments

Comments
 (0)