Skip to content

Design: how a file-change event reaches the parts that hold derived state #415

Description

@Firehed

Summary

When a file changes on disk, every component that holds data derived from that file
must be told. Today that message is passed by hand: one component calls
invalidate() on the next, and each one decides with a type test whether its own
parts take part.

This issue is to decide whether that stays a hand-passed message, or becomes a
general way to announce an event to whoever cares.

What it looks like now

DidChangeWatchedFilesHandler
  -> SymbolSink                     (the interface extends Invalidatable)
    -> DocumentSymbolSink           holds list<Invalidatable>
      -> FilesystemBackend::invalidate
        -> if ($namespaces instanceof Invalidatable)
        -> if ($locator   instanceof Invalidatable)
          -> CompositeSymbolLocator::invalidate
            -> if ($locator instanceof Invalidatable)

Three separate fan-out methods and three type tests, over four packages
(Handler, Knowledge, Index, Cache).

Why it is worth deciding rather than tidying

  • To add a component that holds derived data, an author must find the correct
    parent in that tree and add it there. Missing one is silent: the stale value is
    still served and no test fails.
  • The names describe the response (invalidate) instead of the event (a file
    changed), so every component on the path has to know that something downstream
    caches. SymbolSink extends Invalidatable for this reason alone.
  • More events want the same path later. Server-initiated diagnostics (Design: Diagnostics architecture (errors, warnings, type checking) #266) and
    background workspace indexing (RFC 1 §6) both need "something happened, tell the
    parts that care".

Options to weigh

  1. One registry at the composition root. Everything holding file-derived data
    registers once in KnowledgeStack; a single list is walked on a change. Deletes
    the three fan-out methods and the three type tests. Smallest change; covers only
    this one event.
  2. A published event with subscribers. A real publish/subscribe design, which
    later events reuse. The project prefers PSR interfaces, so PSR-14
    (Event Dispatcher) is the candidate. Larger; needs a decision on who owns the
    dispatcher and how ordering works, if it matters.
  3. Leave it, and only remove the duplication. Tracked separately (see below).

What this issue is not

The narrow removal of the duplicated fan-out is already tracked as manifest row
SC.17, and the duplicated cache-key derivation as SC.18, in
docs/architecture/build-manifest.md. Those can proceed under option 1 or 3
without this issue being settled. This issue exists so the broader question is
recorded rather than decided by default.

There is also a related shape question that is not part of this: symbol backends
take a cache and call it, while namespace catalogs are wrapped by a caching
decorator. Making those match is blocked on the RFC 1 §5.1 locate verb, which
does not exist yet.

Acceptance criteria (for design)

  • A decision recorded between the options above, with the reason
  • Named owner of the fan-out, if one is introduced
  • Stated whether Invalidatable survives, becomes a listener interface, or goes
  • Stated whether the handler keeps calling the write path, or announces an event
  • Stated how a new holder of derived data is registered, and what fails if it is
    not

Issue body generated by AI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions