From e0bd8eea4caa0bd7d5fc28e99f04a4098b8eaf70 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 13:20:59 +0000 Subject: [PATCH] =?UTF-8?q?docs(content):=20correct=20stale=20#1867=20cras?= =?UTF-8?q?h=20notes=20on=20the=20metric=E2=86=92publication=E2=86=92piece?= =?UTF-8?q?=20roll-ups=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit content's `total_*` roll-ups on content_publication (sum of content_metric) and content_piece (sum of content_publication) are already native `Field.summary` fields — the engine recomputes them server-side and, as boot-verified below, the two-level chain cascades correctly. No field conversion is needed here. What was stale: three notes still explained the summary-field choice by claiming a hook "can't do it" because "nested cross-object writes crash / are unsupported in the standalone QuickJS runtime". That is no longer true (framework#1867 is fixed — nested writes from hooks are safe). Reframe the notes to the accurate reason: a *pure aggregate* is best expressed as a declarative, delete-safe `summary` field (the engine captures the child FK pre-image on delete; an afterDelete hook only gets `{ id }`), while a hook is the right tool for a *non-aggregate* cross-object rollup. Also correct the content CHARTER flow table, which still listed a `publication_rollup` flow among five shipping flows — that flow was retired when the totals became summaries; the package ships four flows now (the build confirms "4 Flows"). Boot-verified on a fresh dev server (@objectstack 15.1.1, 0 seed errors): - content_metric → content_publication: e.g. a publication reports total_views 3720 / total_signups 45 summed from its metric rows. - content_publication → content_piece: piece "Why we kept the demo gate…" reports total_views 11370 (= 3720 + 7650 of its two publications) and total_signups 66 (= 45 + 21) — the two-level summary chain is live. Comment/CHARTER-only; no schema, field, or logic change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BZguyAaQbyUpwMZ2gMLaAP --- packages/content/CHARTER.md | 2 +- packages/content/src/hooks/index.ts | 12 ++++++++---- .../src/objects/content_publication.object.ts | 12 +++++++----- 3 files changed, 16 insertions(+), 10 deletions(-) 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',