You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(core): key observation identity and note type to the owning note
Two ways a valid-time query could silently return nothing, both from derived
state answering a question about the note it came from.
**An authored assertion could become permanently unqueryable.** A temporal
qualifier is peeled off an observation before the content is stored, so two
lines that differ only in their qualifier persist identical content and derive
identical synthetic permalinks. The search index is unique on (permalink,
project_id), so `index_entity_markdown` skipped the second as a duplicate --
while its temporal row went on addressing an observation with no search
projection. Querying the second window returned nothing, and every reindex
reproduced the omission from the same markdown:
- [decision] @effective[2026-06-10,2026-07-27) The cache layer will use Redis.
- [decision] @effective[2027-06-10,2027-07-27) The cache layer will use Redis.
Reproduced before the fix: two observation rows, one search row, two
memory_time_index rows, and the 2027 query returning zero results.
The duplicate check is not what is wrong -- it guards a real unique index. Its
input is. Identity is derived after the peel, so it is derived from a string the
note does not consider distinguishing. Nothing on the observation row separates
these two, so no row-local rule can: category, content, context and tags are all
equal, and the qualifier lives in its own projection by an explicit design
decision. A relationship to that projection is not usable either -- all three
readers of `Observation.permalink` read it on *detached* instances after their
session has closed, so a lazy load would raise rather than resolve.
So the ordinal is stored, exactly as `note_section.duplicate_index` already does
for duplicate headings. `replace_observations_for_generation` is the one place
that sees a note's whole observation set in document order, and it counts the
ordinal over `observation_permalink_tail` -- shared with `Observation.permalink`
so the count is taken over exactly the identity the address is built from,
rather than rebuilt inline and drifting (#929). Keying on the generated tail
rather than raw values also closes slug aliasing, where `Foo Bar` and `foo-bar`
are different content that generate one permalink.
The ordinal is 0 for the first observation of any identity, so every permalink
that resolves today is byte-identical afterwards; only later twins gain a
suffix. It fixes the same collision for two observations differing only in
`(context)`, which had the same defect for the same reason.
**A valid-time query combined with `note_types` could not match anything.** A
note's type lives in its frontmatter, so only its entity row carries
`metadata.note_type`; observation rows carry tags and relation rows carry
nothing. Both backends read the type off each row, which asks "is this row an
entity of type X?" when the question was "does this row belong to a note of type
X?". Valid time selects observation rows, so the two predicates were never true
of the same row and the conjunction was unsatisfiable.
Resolved through the owning note instead, in one shared builder both backends
call -- only the JSON accessor differs, and that is all each supplies. Every
search row already carries `entity_id` and an entity row's own `id` equals it,
so one non-correlated membership test covers all three row kinds. Non-correlated
for the reason `temporal_filters` documents: SQLite's `search_index` is an FTS5
virtual table and a correlated EXISTS beside a MATCH is refused outright.
This makes `note_types` return observation and relation rows of matching notes,
where it previously collapsed to entity rows. That is the fix, not a side
effect: restricting which *kind* of row may match is `entity_types`' job, the
two axes are independent, and a query setting neither returns all three kinds.
`test_search_type` asserted the old entity-only shape and is updated -- it
recorded what the defect allowed, not an intention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
0 commit comments