fix(contrail): ingest and index NSID-keyed collections
A collection can be keyed directly by its NSID, with the `collection` field
omitted (the map key is itself the NSID). That shape was only
half-supported: records landed in the records table, but the rest of the
pipeline silently skipped these collections.
- Config: make CollectionConfig.collection optional. resolveConfig
normalizes an omitted value to the map key once; validateConfig accepts
NSID-keyed entries (dotted keys with no collection field). Every
collection-list and lookup helper resolves `collection ?? key`, so
behavior is correct on both raw and resolved configs.
- Ingest: getCollectionNsids / getDiscoverableNsids / getDependentNsids no
longer return undefined, so Jetstream subscription and backfill now
include NSID-keyed collections, and notify resolves shortNameForNsid
instead of rejecting "collection not tracked".
- Search: buildFtsStatements and lookupExistingRecords resolve the storage
key the same way, so FTS is populated and replay/update detection works
for NSID-keyed collections (previously full-text search stayed empty for
them).
- A resolveCollectionKey(config, nsid) helper in contrail-base returns the
storage key (short alias, or the NSID itself when keyed directly), or
null when unknown, de-duplicating the fallback the insert path inlined.
Every change is a type-widening or a fallback that only fires when
`collection` is absent, so existing alias-keyed configs are unchanged.
Tests: config-layer unit tests (resolveConfig / validateConfig for
NSID-keyed entries), the resolve-collection-key helper, an end-to-end
notify-to-ingest test, and the existing search.test.ts (which uses an
NSID-keyed config and was the failing regression). Full suite green.
Changeset included.