Found while building an ObjectStack application in objectstack-ai/duly against published @objectstack/* 17.2.0. Filed here because the fix lands in packages/objectql.
The defect
stripReadonlyFields decides whether a readonly field's value came from the hook or from the caller by comparing Object.is(result[name], supplied[name]). When both are null the comparison cannot distinguish:
- the hook deliberately wrote
null (a clear), from - the hook never touched it, and the caller's
null should be stripped
Both are deleted together, so the hook's clear is silently discarded.
Concrete failure
duly_task has a readonlycompleted_at stamped by a beforeUpdate hook on the transition into done and cleared on the transition out. Reopening a completed task works — unless the caller also sends completed_at: null, which is exactly what a form round-trip of the whole record does. Then:
- the hook sets
completed_at = null - the strip sees
Object.is(null, null) and removes the key - the persisted row keeps its old completion timestamp while
status is in_progress - no error is raised
Measured, not inferred: the record comes back in_progress with a stale completed_at.
Why it is worse than it looks
The corrupted state is exactly the one an application would use a validation rule to make impossible — "a completed task must carry a completion timestamp" cannot catch its inverse, a non-completed task that still carries one. Nothing downstream can tell the row apart from a genuinely completed one, and any on-time metric reading completed_at counts it.
The common paths (a bare { status } reopen, a partial patch) are unaffected, which is why this survives casual testing.
Suggested direction
The distinction the strip needs is provenance, and Object.is on values cannot carry it. Tracking which keys the hook chain actually assigned — rather than inferring it from value equality — is the shape that closes it. A narrower fix would be to treat a key the hook explicitly set as hook-owned regardless of value equality.
Provenance
Reported by a developer agent implementing objectstack-ai/duly#3, and deliberately not worked around in the application hook — a consumer-side hack around a producer defect is how a workaround becomes permanent. That judgement was right, and it is why this is filed here.
Unassigned and untriaged, per the single-producer rule for domain:*.
Found while building an ObjectStack application in
objectstack-ai/dulyagainst published@objectstack/*17.2.0. Filed here because the fix lands inpackages/objectql.The defect
stripReadonlyFieldsdecides whether a readonly field's value came from the hook or from the caller by comparingObject.is(result[name], supplied[name]). When both arenullthe comparison cannot distinguish:null(a clear), fromnullshould be strippedBoth are deleted together, so the hook's clear is silently discarded.
Concrete failure
duly_taskhas areadonlycompleted_atstamped by abeforeUpdatehook on the transition intodoneand cleared on the transition out. Reopening a completed task works — unless the caller also sendscompleted_at: null, which is exactly what a form round-trip of the whole record does. Then:completed_at = nullObject.is(null, null)and removes the keystatusisin_progressMeasured, not inferred: the record comes back
in_progresswith a stalecompleted_at.Why it is worse than it looks
The corrupted state is exactly the one an application would use a validation rule to make impossible — "a completed task must carry a completion timestamp" cannot catch its inverse, a non-completed task that still carries one. Nothing downstream can tell the row apart from a genuinely completed one, and any on-time metric reading
completed_atcounts it.The common paths (a bare
{ status }reopen, a partial patch) are unaffected, which is why this survives casual testing.Suggested direction
The distinction the strip needs is provenance, and
Object.ison values cannot carry it. Tracking which keys the hook chain actually assigned — rather than inferring it from value equality — is the shape that closes it. A narrower fix would be to treat a key the hook explicitly set as hook-owned regardless of value equality.Provenance
Reported by a developer agent implementing
objectstack-ai/duly#3, and deliberately not worked around in the application hook — a consumer-side hack around a producer defect is how a workaround becomes permanent. That judgement was right, and it is why this is filed here.Unassigned and untriaged, per the single-producer rule for
domain:*.