Small correctness/consistency fixes from the design-review backlog that don't warrant individual issues. Items that belong to existing issues went there instead: FK pragma + token-storage follow-through → #46 (comment), malformed-cursor 500s → #53 (comment), revoke()/listGrants() → #57 (comment).
1. Owner via ScopedStack gets entityId stamped, contradicting the spec
Spec §Records: "Absent means owner-created — Records written directly by the stack owner carry no entityId." But ScopedStack.create() stamps unconditionally — "The created record's entityId is always set to the requester" (packages/core/src/stack.ts:880, 892) — so the same owner writing through asEntity(ownerEntityId) (i.e., through any server) produces records that do carry it. Same author, two representations; every "is this owner-authored" check must now know both. Fix: ScopedStack.create() omits entityId when the requester is the owner. Decide + test the same normalization for -own grant checks (hasGrant compares record.entityId === requester, which never matches owner records once normalized — correct, since the owner bypasses grants, but worth a test pinning it). Interacts with #49 (DID identity) and #57 (authorship semantics).
2. timezone: required at init, consumed by nothing
Required at initialize(), stored in _config, surfaced in discovery — and no code path reads it for any behavior. It's a presentation concern living at the data layer. Fix: make it optional at init (default absent, not 'UTC' — a default pretends knowledge), keep it as passthrough app metadata in _config/discovery, and say so in the spec. Removing it entirely is needless churn; requiring it is a lie.
3. date fields validated with bare Date.parse
packages/core/src/validate.ts:78 — the error message promises "Expected ISO 8601 date string" but Date.parse accepts "March 1 2020", "3/1/2020", and other engine-dependent formats, so cross-runtime stacks can disagree about validity and stored values aren't canonical. Fix: validate against an ISO 8601 shape (regex + Date.parse sanity check), matching the message the code already emits. Tightening is a behavior change on paper; per the no-install-base policy (#57) it's free now and only gets more expensive.
4. content: { x: null } filter silently matches nothing
A content filter with a null value matches no records on the SQL adapters (SQL = NULL semantics) — but the natural reading is "records where x is absent/null," and the caller gets an empty result with no signal, the same silent-superset/subset shape as #56. Fix: pick and spec a meaning — leaning null = "field absent or null" (IS NULL / JSON-path-missing semantics) since "match nothing" is never what anyone means; if instead it's rejected, reject loudly (StackValidationError), never silently. Conformance fixture either way (#52).
Work items
Refs #49, #50, #52, #56, #57.
Small correctness/consistency fixes from the design-review backlog that don't warrant individual issues. Items that belong to existing issues went there instead: FK pragma + token-storage follow-through → #46 (comment), malformed-cursor 500s → #53 (comment),
revoke()/listGrants()→ #57 (comment).1. Owner via
ScopedStackgetsentityIdstamped, contradicting the specSpec §Records: "Absent means owner-created — Records written directly by the stack owner carry no
entityId." ButScopedStack.create()stamps unconditionally — "The created record's entityId is always set to the requester" (packages/core/src/stack.ts:880, 892) — so the same owner writing throughasEntity(ownerEntityId)(i.e., through any server) produces records that do carry it. Same author, two representations; every "is this owner-authored" check must now know both. Fix:ScopedStack.create()omitsentityIdwhen the requester is the owner. Decide + test the same normalization for-owngrant checks (hasGrantcomparesrecord.entityId === requester, which never matches owner records once normalized — correct, since the owner bypasses grants, but worth a test pinning it). Interacts with #49 (DID identity) and #57 (authorship semantics).2.
timezone: required at init, consumed by nothingRequired at
initialize(), stored in_config, surfaced in discovery — and no code path reads it for any behavior. It's a presentation concern living at the data layer. Fix: make it optional at init (default absent, not'UTC'— a default pretends knowledge), keep it as passthrough app metadata in_config/discovery, and say so in the spec. Removing it entirely is needless churn; requiring it is a lie.3.
datefields validated with bareDate.parsepackages/core/src/validate.ts:78— the error message promises "Expected ISO 8601 date string" butDate.parseaccepts"March 1 2020","3/1/2020", and other engine-dependent formats, so cross-runtime stacks can disagree about validity and stored values aren't canonical. Fix: validate against an ISO 8601 shape (regex +Date.parsesanity check), matching the message the code already emits. Tightening is a behavior change on paper; per the no-install-base policy (#57) it's free now and only gets more expensive.4.
content: { x: null }filter silently matches nothingA content filter with a
nullvalue matches no records on the SQL adapters (SQL= NULLsemantics) — but the natural reading is "records wherexis absent/null," and the caller gets an empty result with no signal, the same silent-superset/subset shape as #56. Fix: pick and spec a meaning — leaningnull= "field absent or null" (IS NULL/ JSON-path-missing semantics) since "match nothing" is never what anyone means; if instead it's rejected, reject loudly (StackValidationError), never silently. Conformance fixture either way (#52).Work items
ScopedStack.create()owner normalization +-owngrant testtimezoneoptional at init; spec note (data-layer passthrough, no behavior)contentnull-filter semantics in core + both SQL adapters +MemoryAdapter(Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50) + fixture (PATCH contract mismatch: spec defines content-only merge patch, APIAdapter sends record-field envelopes #52)Refs #49, #50, #52, #56, #57.