Repository navigation
GitHub sync: git→mesh import silently FullReplace-prunes mesh-only nodes (no paths logged, no opt-out for bidirectional Spaces) #604
Description
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? unknownSo 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 sourceThe 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
InMemoryStaticRepoSourcedefault toAdditive(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 toAdditiveforBidirectionaland allowingFullReplacefor 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
- Configure a Space with GitHub sync,
Direction = Bidirectional. - Create a node on the mesh (e.g. a new NodeType + instance) and do not commit it to the branch.
- Run "Update to latest" (
git_hub_sync op:update), or let any catch-up import fire. - 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
GitSync: no-op sync never advances the conflict horizon; bidirectiona…
Merge pull request #895 from Systemorph/fix/677-604-gitsync-horizon
Name HOW the frozen sources' arm is known, not just which arm