Skip to content

GitHub sync: git→mesh import silently FullReplace-prunes mesh-only nodes (no paths logged, no opt-out for bidirectional Spaces) #604

Closed

Description

@rosalie1984

Summary

A git → mesh import of a GitHub-synced Space runs in FullReplace (mirror) mode and, in doing so, silently deletes any mesh node that is absent from the branch tree — logging only an aggregate count, never the paths. For a Space that is edited both on the mesh AND via GitHub sync (the default SyncDirection.Bidirectional), a node created on the mesh but not yet committed to the branch is destroyed by the next unrelated "Update to latest" / catch-up import, with no record of what was removed.

Two independent gaps combine here: a visibility gap (the prune names nothing) and a default-policy gap (GitSync inherits FullReplace where the AI catalogs deliberately opt out of it). Either alone would have made the incident far less damaging.

1 · Prune is silent — it logs a count, not the paths (visibility)

The importer computes prunable nodes and deletes each one, but the only per-path logging is on the failure branch. A successful prune produces just an aggregate count in the activity summary:

// StaticRepoImporter.cs — the prune loop
toPrune.Select(t => AsSystem(hub, () => meshService.DeleteNode(t.Path))
    .Select(_ => 1)
    .Catch<int, Exception>(ex => {
        logger?.LogWarning(ex,
            "[StaticRepoImport] {Partition}: prune of {Path} failed (continuing).",
            source.Partition, t.Path);            // ← path logged ONLY on failure
        return Observable.Return(0);
    }))
...
var summary = $"Imported {count.Imported} node(s), pruned {prunedCount}, synced {contentCount} content file(s).";
//                                              ^^^^^^^^^^^ count only — which nodes? unknown

So the activity log for a destructive import reads … pruned 7 … with no way to learn which seven nodes were deleted. When a page/feature "disappears," there is no breadcrumb tying its removal to the import.

Ask: emit the pruned paths into the activity log (an Information/Warning line per pruned path, or a single line listing them). A prune is a destructive act on user data — it should be as auditable as the ⚠ Failed to import … lines already are.

Ref: src/MeshWeaver.Graph/StaticRepoImporter.cs — prune loop (~line 639) and the summary message (~line 674).

2 · GitHub sync imports default to FullReplace (policy)

IStaticRepoSource.SyncMode defaults to PartitionSyncMode.FullReplace:

// IStaticRepoSource.cs
PartitionSyncMode SyncMode => PartitionSyncMode.FullReplace;   // mirror the partition to the source

The AI catalog sources all override to Additive precisely because they are user-mutable partitions where a config re-import must not nuke user-added entries:

src/MeshWeaver.AI/ModelStaticRepoSource.cs:   SyncMode => PartitionSyncMode.Additive;
src/MeshWeaver.AI/SkillStaticRepoSource.cs:   SyncMode => PartitionSyncMode.Additive;
src/MeshWeaver.AI/AgentStaticRepoSource.cs:   SyncMode => PartitionSyncMode.Additive;
src/MeshWeaver.AI/HarnessStaticRepoSource.cs: SyncMode => PartitionSyncMode.Additive;

But the GitHub-sync source does not override it:

// InMemoryStaticRepoSource.cs — no SyncMode member → inherits FullReplace
internal sealed class InMemoryStaticRepoSource(
    string partition, IReadOnlyList<MeshNode> children, MeshNode? root) : IStaticRepoSource
{
    public string Partition => partition;
    public bool Versioned => false;
    public IReadOnlyList<MeshNode> EnumerateSourceNodes() => children;
    public MeshNode? PartitionRoot => root;
    // SyncMode not overridden ⇒ PartitionSyncMode.FullReplace
}

So every git → mesh import of a Space runs full-mirror, and there is no configuration surface to change it: GitHubSyncConfig exposes Direction, Branch, Subdirectory, Create*IfMissing — but no prune policy.

FullReplace is a surprising default for a bidirectional, user-facing Space sync. It is the right default for a one-way "the repo is the source of truth" mirror, but with Direction == Bidirectional the mesh is also an editing surface, and full-mirror-prune means "anything you created here but haven't pushed yet is deleted on the next pull."

Ask (pick one):

  • Make InMemoryStaticRepoSource default to Additive (prune only nodes the source previously owned per the manifest — user-added nodes survive), or
  • Add a prune-policy field to GitHubSyncConfig (e.g. SyncMode / PruneOnImport) surfaced in the GitHub Sync settings tab, defaulting to Additive for Bidirectional and allowing FullReplace for an explicit one-way mirror.

Ref: src/MeshWeaver.GitSync/InMemoryStaticRepoSource.cs; src/MeshWeaver.Mesh.Contract/IStaticRepoSource.cs:40; src/MeshWeaver.Mesh.Contract/PartitionSyncMode.cs; src/MeshWeaver.GitSync/GitHubSyncConfig.cs.

Repro

  1. Configure a Space with GitHub sync, Direction = Bidirectional.
  2. Create a node on the mesh (e.g. a new NodeType + instance) and do not commit it to the branch.
  3. Run "Update to latest" (git_hub_sync op:update), or let any catch-up import fire.
  4. The mesh-only node (and its subtree) is deleted. The import activity log says … pruned N … — but not which nodes, so the deletion is effectively invisible until someone notices the feature is gone.

Why it matters

A bidirectional Space sync invites editing on both sides, but the import silently treats the branch as the sole source of truth and destroys anything mesh-only — with no log naming the casualties. The two fixes are complementary: (1) makes the destruction auditable when it happens; (2) stops it from happening by surprise. Together they turn "my page vanished and nothing says why" into either a safe no-op or a clearly-logged, opted-into action.

Activity

  1. added a commit that references this issue on Aug 7, 2026
    6087950
  2. added a commit that references this issue on Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions