diff --git a/packages/content/README.md b/packages/content/README.md index 8a10544..f777539 100644 --- a/packages/content/README.md +++ b/packages/content/README.md @@ -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`) | @@ -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`). diff --git a/packages/content/SPEC.md b/packages/content/SPEC.md index da31952..2c0beee 100644 --- a/packages/content/SPEC.md +++ b/packages/content/SPEC.md @@ -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):** diff --git a/packages/content/src/objects/content_publication.object.ts b/packages/content/src/objects/content_publication.object.ts index 9c3462a..53ec4e9 100644 --- a/packages/content/src/objects/content_publication.object.ts +++ b/packages/content/src/objects/content_publication.object.ts @@ -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({