Skip to main content

Module cursor

Module cursor 

Source
Expand description

Source-cursor driver — assemble a SourceCursor from live workspace state, so the brief’s changed-slice preface can steer a pass at what moved.

Engine-side port of the plugin’s computeSourceCursor (inject.mjs). For each of a binding’s source facets it resolves the change-detection strategy, reads the durable baseline from the destination mem’s sync_state (keyed "<binding-id>/<facet-or-refmem>#synced", D4), computes the changed slice against the source’s current state, and unions the per-facet slices.

Strategies:

  • git — diff the stored commit id against the source tree’s current HEAD (subprocess git rev-parse / git diff --name-status), with the facet scope + ingest deny_paths pushed down as :(glob) / :(glob,exclude) pathspecs.
  • graph — diff the source mem’s snapshot token via the engine’s own Engine::changes_since; reference mems are graph-detected too.
  • mtime — enumerate the facet’s files (minus the facet scope’s own denies and the ingest deny_paths, applied identically to the git strategy’s exclude pathspecs — see enumerate_facet_files), compute a stat-map digest, memoise it under .memstead.cache/ingest/source-cursor/, and diff the current digest against the memoised baseline via the pure super::slice::mtime_slice_outcome core (precise, incl. deletions).

Deny invariance. Ingest deny_paths are enforced identically by every strategy that reads a file tree — git, mtime, and refinement’s enumeration, plus both token computations (current_primary_token / source_moved). A file matching a deny_paths entry appears in no changed slice, no refinement batch, and never influences the mtime digest or the source_moved token. The graph strategy is exempt by definition: deny_paths entries are file-path globs, but a graph source’s artifacts are entities (entity-granular), so a file-path glob can never select one. This exemption is designed, not an omission.

One deny dialect. A deny_paths entry is a workspace-relative glob — the exact grammar and resolution root as a facet-scope entry, resolved by the same [build_glob_set] / :(glob,exclude) machinery. The plugin’s PreToolUse deny hook enforces the identical dialect against the ingest agent’s Read/Glob/Grep, reading the active list from an engine-written cache file. write_active_deny_file publishes that file during brief rendering (remove-then-write, overwrite-always), so hook enforcement tracks the last rendered ingest and is never stale. A deny entry that selects no file in the project tree is surfaced as a rendered brief warning (SourceCursor::dead_denies) rather than silently no-op’ing — catching typos and un-migrated legacy bare names, never a hard error.

One empty-scope semantic. A facet with no allow patterns is unscoped — and that is a typed refusal, identical on every file-tree strategy: git, mtime, and refinement all decline to diff or enumerate the whole medium (a facet_unscoped check gates it). No strategy silently emits an empty slice, enumeration, or batch for an unscoped facet; instead the source contributes NoSignalReason::Unscoped, which renders in the brief. A facet that genuinely wants the whole medium writes **/*. This is a different field from the ingest’s deny_paths: an empty deny_paths list is valid and means “no denies” — it never trips the unscoped refusal.

Visible no-signal. Every source contributes a per-source outcome. A genuinely-unchanged source (baseline present, nothing moved) stays silent — the only documented silence, preserving the “brief is byte-identical to a plain roam when nothing moved” property. Every other no-signal condition — unscoped facet, signal:none, git failure / unknown baseline, missing graph snapshot — is collected as a NoSignalNote and rendered distinguishably.

Load-bearing invariant: the new baseline token is only collected here (into write_commands / reseed); it is recorded by the engine’s set_mem_sync_state writer when projection advance completes a full pass (D7). The driver never writes it.

Functions§

compute_source_cursor
Assemble the combined SourceCursor for an ingest from live state: the destination mem’s sync_state baselines and each source’s current state.
enumerate_facet_files
Enumerate the workspace-relative file paths a primary source’s facet scope selects — the mtime strategy’s input set. Mirrors the plugin’s enumerateFacetFiles: only codebase/filesystem mediums; the facet’s allow globs minus its deny globs, evaluated over the medium’s directory tree. Returns a sorted, de-duplicated list. An unscoped facet (no allows) yields an empty list here — but callers must not treat that as signal: the strategy layer (compute_mtime_slice / current_primary_token) refuses an unscoped facet via facet_unscoped before enumerating, so the empty list is only ever reached for a genuinely-empty scoped enumeration.
medium_base
The medium pointer resolved to an absolute base directory. Public so init-time surfaces (CLI projection init) can resolve a medium base exactly as the strategies do — e.g. to warn when it falls outside the workspace root.
source_moved
Whether any of an ingest’s sources moved since its last synced pass — the cheap, slice-free predicate the backoff uses as its additive second trigger. Compares each source’s current token to the baseline stored in the destination mem’s sync_state; a source with no baseline is not “moved” (a first sync does not by itself defeat backoff). Mirrors the plugin’s sourceChangedSince.
source_moved_since
The generalized form of source_moved: compare each source’s current change-detection token against the baseline stored under "<binding>/<facet>#<state>" in the destination mem’s sync_state. The state suffix selects the baseline family — "synced" (the build/sync baseline source_moved reads) or "verified" (the verify baseline).
write_active_deny_file
Write the active ingest’s deny list for the plugin hook, stale-safe.