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
14 changes: 7 additions & 7 deletions packages/content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ rm -rf packages/content/.objectstack
| Business objects | 9 (`piece`, `topic`, `signal`, `competitor`, `channel`, `publication`, `metric`, `cta`, `template`) |
| State machines | 2 (`piece.status` 8-state, `signal.status` 3-state) |
| Approvals | 1 (`publish_approval`: gates `in_review → approved`) |
| Flows | 4 (signal → topic, default CTA, publication rollup placeholder, lifecycle timestamps) |
| Flows | 4 (signal → topic, default CTA, publish approval, lifecycle notifications) |
| Dashboards | 3 (Today's Workbench / Editorial Calendar / Channel ROI) |
| App | 1 (Content Ops, 7 nav items) |
| Views | 7 (incl. kanban on `piece.status`, calendar by `publish_at`) |
Expand Down Expand Up @@ -135,12 +135,12 @@ the charter.

## Known v0 caveats

- The `publication_rollup` flow uses a `script` node placeholder (flow `aggregate`
nodes aren't in v6 spec yet). Live rollups happen via **`metricRollupHook`
+ `publicationRollupHook`** in `src/objects/content_rollup.hook.ts`: every
metric insert/update deltas into `publication.total_*`, then up into
`piece.total_*`. Seed values are pre-populated so dashboards have data
before any metric is recorded.
- Performance rollups are native **`Field.summary`** fields (#1870), not a flow
or hook: the engine recomputes `publication.total_*` from the child
`content_metric` rows on every metric write, and those cascade one level up into
`piece.total_*`. A piece with no publications yet has no child to trigger a
recompute, so its `total_*` read `null` (not `0`) until its first publication
exists — cosmetic, and the seeded published pieces all have publications.
- Today's Workbench KPI tiles filter on `assignee == {current_user_id} OR
assignee == null`, so unassigned seed pieces show up immediately. Newly
created pieces auto-assign to the creator (see `content_piece.hook.ts`).
Expand Down
8 changes: 6 additions & 2 deletions packages/content/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,12 @@ action on the record page instead of a background flow.

1. `signal_to_topic_promotion` — on `signal.status → promoted`, create a `content_topic` linked back.
2. `cta_creation_default` — on `content_piece` create, if `cta_count = 0`, create one default CTA from the channel's default goal.
3. `publication_rollup` — on `content_metric` insert/update, refresh `publication.total_views / total_clicks / total_signups / total_revenue` denormalized fields.
4. `stamp_lifecycle_timestamps` — on `piece.status` transitions, stamp `submitted_at / approved_at / published_at / archived_at` (todo template's pattern).
3. `publish_approval` — gates the `in_review → approved` transition through an approval.
4. `stamp_lifecycle_timestamps` — on `piece.status` transitions, notify the editor/writer/owner at the right moment.

> `publication.total_*` / `piece.total_*` are native `Field.summary` roll-ups
> (#1870), recomputed server-side from child `content_metric` rows and cascaded
> one level up to the piece — not a flow.

**Manual actions (button on record page):**

Expand Down
10 changes: 7 additions & 3 deletions packages/content/src/objects/content_publication.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import { tmpl } from '@objectstack/spec';
* entry in `piece.target_channels`).
*
* Metric snapshots are recorded against this row, not the piece — so a
* blog post + LinkedIn cross-post are measured independently. The
* `total_*` numbers are STORED fields (seed/client-maintained); cross-object
* rollups via hook are unsupported in the standalone runtime — see
* blog post + LinkedIn cross-post are measured independently. The `total_*`
* numbers are native `Field.summary` roll-ups (#1870): the engine recomputes
* them server-side from the child `content_metric` rows, and they cascade one
* level up to the parent `content_piece`'s own `total_*` summaries. (A piece
* with no publications yet has no child to trigger a recompute, so its rollups
* read `null` until its first publication exists.) A hook can't maintain these —
* a nested cross-object write is unsupported in the standalone runtime; see
* content/src/hooks/index.ts.
*/
export const Publication = ObjectSchema.create({
Expand Down
Loading