diff --git a/packages/content/CHARTER.md b/packages/content/CHARTER.md index 7a7e6d8..8cdd8d9 100644 --- a/packages/content/CHARTER.md +++ b/packages/content/CHARTER.md @@ -65,7 +65,7 @@ These exist so the template stays a **template**, not a half-finished product: | Total `.ts` LOC under `src/` | ≤ 6,000 | Readable in a half-day sitting | | Locales | 2 (`en`, `zh-CN`) | Proves multi-locale without becoming a translation project | | Dashboards | 3 | Today / Calendar / ROI — each shows a distinct widget class | -| Flows | ≤ 5 | Event-driven only (no cron in spec v6); time-based work ships as record actions. Cap raised 4→5 (2026-06): the package already ships five distinct event-driven flows — signal→topic promotion, CTA default, publish approval, publication rollup, and lifecycle notifications (the latter was renamed from `stamp_lifecycle_timestamps` once timestamp-stamping moved into the piece hook). | +| Flows | ≤ 5 | Event-driven only (no cron in spec v6); time-based work ships as record actions. The package ships four event-driven flows — signal→topic promotion, CTA default, publish approval, and lifecycle notifications (renamed from `stamp_lifecycle_timestamps` once timestamp-stamping moved into the piece hook). A former fifth, `publication_rollup`, was retired once the metric→publication→piece totals became native `Field.summary` roll-ups (its script-node "aggregations" never ran — the automation engine has no aggregate node). | | State machines | 2 | piece + signal | | Approval processes | 1 | publish gate | | Sharing rules | 1 | topic-scoped | diff --git a/packages/content/src/hooks/index.ts b/packages/content/src/hooks/index.ts index 2f1af87..5ff6662 100644 --- a/packages/content/src/hooks/index.ts +++ b/packages/content/src/hooks/index.ts @@ -4,8 +4,12 @@ import { pieceHook, signalHook } from '../objects/content_piece.hook'; // NOTE: the metric → publication → piece totals (`total_views` etc.) are native // `Field.summary` roll-ups on content_publication / content_piece (#1870) — the -// engine recomputes them server-side on child insert/update/delete. They are NOT -// hook-maintained: a cross-object rollup write from a hook is unsupported in the -// standalone QuickJS runtime (nested writes crash; `ctx.services.data` is -// undefined inside it), which is why the declarative summary-field route is used. +// engine recomputes them server-side on child insert/update/delete. A pure +// sum/max like these is expressed declaratively as a `summary` field rather than +// a hook because it needs to be delete-safe: the engine captures the child's FK +// pre-image on delete and recomputes the parent, whereas an `afterDelete` hook +// only receives `{ id }` and can't tell which parent to fix. (Nested cross-object +// writes from hooks are now safe — framework#1867 is fixed — so a hook is the +// right tool for a *non-aggregate* rollup; for these pure aggregates the summary +// field stays the better fit.) export const allHooks = [pieceHook, signalHook]; diff --git a/packages/content/src/objects/content_publication.object.ts b/packages/content/src/objects/content_publication.object.ts index f750c34..32d6c42 100644 --- a/packages/content/src/objects/content_publication.object.ts +++ b/packages/content/src/objects/content_publication.object.ts @@ -13,8 +13,9 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * 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 + * read `null` until its first publication exists.) These are declared as + * `summary` fields rather than hook-maintained because a pure aggregate needs a + * delete-safe recompute — which the engine does natively off the child FK; see * content/src/hooks/index.ts. */ export const Publication = ObjectSchema.create({ @@ -67,9 +68,10 @@ export const Publication = ObjectSchema.create({ // recomputes these server-side whenever a child `content_metric` row is // inserted/updated/deleted (FK `content_metric.publication` auto-detected). // This replaces the old `publication_rollup` flow, whose script-node - // "aggregations" never ran (the automation engine has no aggregate node) — - // and a hook can't do it either (nested cross-object write is unsupported in - // the standalone QuickJS runtime; see content/src/hooks/index.ts). + // "aggregations" never ran (the automation engine has no aggregate node). A + // hook could do it now (nested cross-object writes are safe since + // framework#1867), but a `summary` field is the delete-safe, declarative fit + // for a pure aggregate; see content/src/hooks/index.ts. total_views: Field.summary({ label: 'Views', group: 'rollups',