From 629547363ef71e4a50831c8a00ee496fa3ad14fc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:13:20 +0000 Subject: [PATCH] docs(project): correct stale #1867 crash notes on actual_cost / progress_percent (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlike the other four #84 fields, neither project field is a reconstructible child aggregate, so there is nothing to convert to a summary or hook roll-up — only the stale framing to fix. Investigation (boot-inspected the seed): - `actual_cost` (105000 / 340000 / 0) has NO child cost source. pm_timesheet records `hours` only; cost = hours × a person/role rate, and no rate column exists in the 4-object CHARTER schema. It is an externally-computed actual, not a rollup a summary or hook could rebuild. - `progress_percent` (38 / 55 / 0) is NOT a milestone ratio. Both 2-milestone projects have the same status mix (1 completed + 1 in_progress = 50%), and issues are 0% — so any child aggregate flattens the hand-tuned 38/55 to a single value. It is a curated delivery metric, not a naive count. What changed (comments/descriptions only, no logic/schema change): the notes in pm_project.hook.ts, pm_project.object.ts (both field descriptions) and pm_timesheet.view.ts claimed these are stored "because a nested write crashes the QuickJS sandbox". That is no longer true (framework#1867 is fixed). Reframe them to the real reason — actual_cost has no child cost column to sum; progress_percent is a curated metric a child count would flatten — and note that nested hook writes are now safe. Follow-up left to the maintainer: `progress_percent` COULD be made a live milestone-completion roll-up via an afterInsert/afterUpdate hook (the #1867 fix enables it), but only if count-based progress is acceptable — it would re-seed 38/55 → 50/50 and lose the per-project nuance. Flagged in the field note rather than done here, since it changes shipped demo semantics. Build-verified (tsc + build, 6 objects / 72 fields); format:check clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BZguyAaQbyUpwMZ2gMLaAP --- packages/project/src/objects/pm_project.hook.ts | 15 ++++++++++----- packages/project/src/objects/pm_project.object.ts | 13 +++++++++---- packages/project/src/views/pm_timesheet.view.ts | 3 ++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/project/src/objects/pm_project.hook.ts b/packages/project/src/objects/pm_project.hook.ts index 79d8d24..1ad6b00 100644 --- a/packages/project/src/objects/pm_project.hook.ts +++ b/packages/project/src/objects/pm_project.hook.ts @@ -12,11 +12,16 @@ import type { Hook, HookContext } from '@objectstack/spec/data'; * and group on a single signal. * * NOTE — this hook only mutates the incoming `input` payload; it never issues a - * nested `ctx.api` write. Hook bodies share one QuickJS (asyncify) sandbox that - * permits a single suspended async call, so a nested engine write crashes the - * process. That is why `progress_percent` / `actual_cost` (which would need a - * cross-object rollup from milestones / timesheets) are stored fields kept in - * sync by seed/client, not by this hook. + * nested `ctx.api` write. Nested cross-object writes from a hook are safe now + * (framework#1867 is fixed), but `progress_percent` / `actual_cost` are still + * deliberately NOT hook roll-ups — neither is a plain aggregate of a child field: + * - `actual_cost` is an externally-computed actual (hours × a person/role rate). + * `pm_timesheet` records hours only; there is no child cost column to sum, and + * the per-rate data lives outside the 4-object CHARTER schema. + * - `progress_percent` is a curated delivery metric, not a naive completed/total + * milestone count (the seeded per-project values differ where a count would + * flatten them to the same number). + * Both are kept by seed/client; see each field's note in pm_project.object.ts. * * IMPORTANT — the handler runs **body-only** in the QuickJS sandbox: only the * function body ships, so module-scope helpers are NOT in scope at runtime. diff --git a/packages/project/src/objects/pm_project.object.ts b/packages/project/src/objects/pm_project.object.ts index bc1cdae..2dac9a7 100644 --- a/packages/project/src/objects/pm_project.object.ts +++ b/packages/project/src/objects/pm_project.object.ts @@ -181,8 +181,11 @@ export const Project = ObjectSchema.create({ required: false, group: 'budget', description: - 'Cost to date. Maintained by seed/client or an out-of-sandbox worker — NOT a live ' + - 'hook rollup (a nested write from a timesheet hook crashes the QuickJS sandbox).', + 'Cost to date — an externally-computed actual (hours × a person/role rate). ' + + 'pm_timesheet records hours only, so there is no child cost column to sum: this is ' + + 'not a summary/hook roll-up but a value set by seed/client or a finance worker. ' + + '(Nested hook writes are safe now — framework#1867 — the blocker is the missing rate, ' + + 'not the sandbox.)', }), // Team @@ -206,8 +209,10 @@ export const Project = ObjectSchema.create({ min: 0, max: 100, description: - 'Share of completed milestones. Maintained by seed/client (not a live hook ' + - 'rollup — see actual_cost note).', + 'Delivery progress (0–100). A curated metric, not a naive completed/total milestone ' + + 'count — the seeded per-project values differ where a count would flatten them — so it ' + + 'is kept by seed/client. It could be made a live milestone roll-up via an afterInsert/' + + 'afterUpdate hook now that framework#1867 is fixed, if count-based progress is acceptable.', }), }, diff --git a/packages/project/src/views/pm_timesheet.view.ts b/packages/project/src/views/pm_timesheet.view.ts index c70bf8f..6a5d4d4 100644 --- a/packages/project/src/views/pm_timesheet.view.ts +++ b/packages/project/src/views/pm_timesheet.view.ts @@ -4,7 +4,8 @@ import { defineView } from '@objectstack/spec/ui'; /** * Timesheet views — daily effort entries, grouped by project, with a billable - * tab. Timesheets feed the project's actual_cost (maintained out of sandbox). + * tab. Timesheets record effort (hours); the project's actual_cost is computed + * externally (hours × rate), not rolled up from these rows. */ export const TimesheetViews = defineView({ list: {