Found while implementing #16106 (the field-typed arm of filter-preset-comparand; PR pending on branch claude/issue-16106-preset-comparand-field-typed). Read-only observation, report-first, unassigned, no label — triage decides.
What
@objectstack/lint's object graph (packages/lint/src/object-graph.ts, GraphObject.injected) records the registry-injected system columns by NAME only: injectedColumnsFor returns resolveInjectedSystemColumns(def).names (packages/spec/src/data/injected-system-columns.ts). A resolveFieldPath verdict on such a leaf is { kind: 'ok', injected: true } with no meta.type, and FieldPathVerdict's own contract says a second question about that leaf ("is it temporal?") is unanswerable there.
Consequence for the arm #16106 lands: on a dashboard widget bound to a dataset on crm_opportunity, close_date: 'last_30_days' (an author-declared date column) is refused at lint time with the window named, while created_at: 'last_30_days' on the same widget passes lint and the runtime publish gate — and created_at / updated_at are the most common temporal filter columns an author reaches for.
Measured
Worktree at origin/main 4a1a3b0c25, ObjectQL + @objectstack/driver-memory, an object declaring no created_at of its own (the registry injects it):
where |
engine |
{ created_at: 'last_30_days' } |
REFUSED INVALID_FILTER / 400 — … compares a declared datetime field against "last_30_days" at where.created_at … |
{ updated_at: { $in: ['last_30_days'] } } |
REFUSED INVALID_FILTER / 400 — … at where.updated_at.$in[0] … |
{ created_at: { $gte: '{30_days_ago}' } } |
200, 30 rows (positive control) |
So the engine door refuses it with the registry's field map in hand; only the authoring-time layer cannot see the type. Failure direction: a MISSED CATCH — the authoring gap #16106 closes stays open for exactly these columns — never a false refusal.
Where a fix would go (not decided here)
resolveInjectedSystemColumns is the one derivation the registry's applySystemFields consumes. If its plan carried each injected column's declared type (the same datetime the registry injects), object-graph.ts could expose it on GraphObject.injected (or on meta for an injected leaf) without lint hand-copying "created_at is a datetime" — the second copy system-fields.ts's header forbids. Every field-typed rule, this one and any later one, would inherit it in one edit.
Dedupe
Searched "lint object graph registry-injected system columns created_at updated_at carry no field type": 5 hits (#16119, #15922, #16083, #16118, #8116). #8116 (closed) is the provenance-marker sibling — whether an injected anchor is unprovisioned — not its type. None names this.
Follow-up to #16106; not blocked by it.
Found while implementing #16106 (the field-typed arm of
filter-preset-comparand; PR pending on branchclaude/issue-16106-preset-comparand-field-typed). Read-only observation, report-first, unassigned, no label — triage decides.What
@objectstack/lint's object graph (packages/lint/src/object-graph.ts,GraphObject.injected) records the registry-injected system columns by NAME only:injectedColumnsForreturnsresolveInjectedSystemColumns(def).names(packages/spec/src/data/injected-system-columns.ts). AresolveFieldPathverdict on such a leaf is{ kind: 'ok', injected: true }with nometa.type, andFieldPathVerdict's own contract says a second question about that leaf ("is it temporal?") is unanswerable there.Consequence for the arm #16106 lands: on a dashboard widget bound to a dataset on
crm_opportunity,close_date: 'last_30_days'(an author-declareddatecolumn) is refused at lint time with the window named, whilecreated_at: 'last_30_days'on the same widget passes lint and the runtime publish gate — andcreated_at/updated_atare the most common temporal filter columns an author reaches for.Measured
Worktree at
origin/main4a1a3b0c25,ObjectQL+@objectstack/driver-memory, an object declaring nocreated_atof its own (the registry injects it):where{ created_at: 'last_30_days' }INVALID_FILTER/ 400 —… compares a declared datetime field against "last_30_days" at where.created_at …{ updated_at: { $in: ['last_30_days'] } }INVALID_FILTER/ 400 —… at where.updated_at.$in[0] …{ created_at: { $gte: '{30_days_ago}' } }So the engine door refuses it with the registry's field map in hand; only the authoring-time layer cannot see the type. Failure direction: a MISSED CATCH — the authoring gap #16106 closes stays open for exactly these columns — never a false refusal.
Where a fix would go (not decided here)
resolveInjectedSystemColumnsis the one derivation the registry'sapplySystemFieldsconsumes. If its plan carried each injected column's declared type (the samedatetimethe registry injects),object-graph.tscould expose it onGraphObject.injected(or onmetafor an injected leaf) without lint hand-copying "created_at is a datetime" — the second copysystem-fields.ts's header forbids. Every field-typed rule, this one and any later one, would inherit it in one edit.Dedupe
Searched "lint object graph registry-injected system columns created_at updated_at carry no field type": 5 hits (#16119, #15922, #16083, #16118, #8116). #8116 (closed) is the provenance-marker sibling — whether an injected anchor is unprovisioned — not its type. None names this.
Follow-up to #16106; not blocked by it.