@@ -565,10 +565,44 @@ export function stripReadonlyWhenFieldsMulti(
565565 * ownership here makes it enforced rather than merely asserted — the same
566566 * `declared ≠ enforced` correction as #4447 (`created_at`), one type over.
567567 *
568- * Deliberately NOT `formula` / `summary`: those are computed on read from a
569- * plan, never stored from the write payload, so there is no caller value to
570- * strip. Keep this set to types whose value is (a) persisted and (b) issued by
571- * the runtime.
568+ * Deliberately NOT `formula`: a formula field IS computed on read from a plan
569+ * (`applyFormulaPlan`) and never stored from the write payload, so there is no
570+ * caller value to strip in the first place.
571+ *
572+ * Deliberately NOT `summary` either — but for a COMPLETELY DIFFERENT reason,
573+ * and conflating the two is what this note exists to prevent (#6014). A roll-up
574+ * `summary` is NOT computed on read: it is a real stored column the runtime
575+ * maintains. `ObjectQL.recomputeSummaries` writes it with an ordinary
576+ * `update(parent, { [summaryField]: value })` after any child write, and since
577+ * #5749 / PR #6013 `initializeSummaryFields` also seeds it at parent INSERT.
578+ * Reads hit that stored column directly — which is exactly why
579+ * `["task_count","=",0]` is an in-database comparison, and why a never-seeded
580+ * `null` silently dropped rows from it (#5749). So `summary` satisfies BOTH
581+ * clauses a naive membership rule would use — persisted AND runtime-issued —
582+ * and is STILL excluded. Persistence and runtime ownership do not decide it.
583+ *
584+ * What decides it is the third clause: a runtime-owned type must have NO
585+ * legitimate caller-supplied value. `autonumber` qualifies — a client-chosen
586+ * record number bypasses the sequence and forges a business identifier nothing
587+ * later corrects. `summary` does not qualify: the value is a derived cache of
588+ * the child aggregate, self-healing on the next child write, and supplying an
589+ * initial value is a SUPPORTED authoring path — `initializeSummaryFields`
590+ * deliberately keeps a caller-supplied one ("author supplied a value"), so
591+ * historical imports and seed data may carry pre-computed totals.
592+ *
593+ * DO NOT "fix the code to match this comment" by adding `summary` here. The
594+ * insert-side strip ({@link stripRuntimeOwnedFields}) keys on the RAW caller
595+ * payload and runs in `engine.insert` AFTER the seed pass, so a plain
596+ * (non-`isSystem`, non-`preserveAudit`) import of a parent carrying
597+ * `task_count: 42` would lose the 42 to the strip and get no 0 from the seed
598+ * either — the seed already skipped that field precisely BECAUSE the caller
599+ * supplied it. The column lands `null`, the exact state #6013 was written to
600+ * eliminate, and the write still reports success. Only `isSystem: true` (whole
601+ * pass skipped) or `preserveAudit: true` (kept by {@link isPreservableUnderAudit},
602+ * since a summary field is not `system: true`) would survive it.
603+ *
604+ * Keep this set to types whose value is (a) persisted, (b) issued by the
605+ * runtime, and (c) never legitimately supplied by a caller.
572606 */
573607const RUNTIME_OWNED_FIELD_TYPES : ReadonlySet < string > = new Set ( [ 'autonumber' ] ) ;
574608
0 commit comments