D171 — A cross-KB collision is an error, not an alphabetical tie-break¶
Decision. provisioning.DetectCollisions reports every kind+name claimed
by two or more distinct kb: sources, and MergeArtifactsStrict turns that into
a *CollisionError naming the kind, the name and the claiming KBs. The client
merges through the strict variant (fetchMergedManifest), so a collision stops
the sync before anything is materialized. MergeArtifacts keeps its tolerant
behaviour for BuildManifest. cartographer client bind warns when a new
binding creates a collision, and doctor gains a kb-collisions check.
Rationale.
- The silent resolution was unstable, not just undocumented.
MergeArtifactsdeduplicates onkind+namewithout the source, andpreferArtifactpicked the alphabetically firstkb:source. Nothing recorded that a second copy existed. Worse, unmounting the winning KB makes the losing one appear: the content an agent reads changes with no artifact, no hash and no revision having changed in the KB anyone was looking at. - Error, not warning. A warning about an artifact the agent then actually loads is worse than a failed sync, because at that point the wrong answer is silent. The failure names both KBs and the remedy, which is a rename.
- KB-over-bundle stays. It is deliberate — a KB may override a bundled skill — and unambiguous, because the bundle is single. Only KB↔KB became an error.
- Two functions, not a flag.
BuildManifestmerges one KB plus the bundle, where the case cannot arise; giving it a strictness parameter would add a branch nobody can reach.MergeArtifactsStrictis the client's entry point and the tolerant one keeps its callers. fetchMergedManifestsplit intofetchCandidates+ merge. The per-KB responses have to survive the pull for the collision to be attributable at all, and the same unmerged candidates are whatclient bindanddoctorfilter per provider. This is also the seam the filtered projection needs.- Per provider, not globally. Two colliding KBs bound to two different
providers are not a conflict.
collisionsForProvidernarrows a collision to the KBs bound to one provider, so the warning does not train an operator to ignore it. client bindstays offline. The collision check needs the server, so it is best-effort: unreachable means "check skipped", not a failed command. Configuring a machine must not require the network, and the sync-time refusal is the backstop.
Known limitation, since closed by D172. As written here, runSync
rewrote the providers' MCP entries before fetching the manifest, so a refused
merge still left those rewritten; only artifacts and the lockfile were protected.
A test pinned that behaviour so the reorder could not land silently. D172
reordered runSync and inverted the assertion: a refused merge now leaves the
MCP entries untouched as well.
Consequences. A deployment with two colliding KBs — working today, silently and arbitrarily — starts failing its sync with a report and a remedy. That is the intent. Everything else is unchanged: single-KB clients and clients whose KBs have distinct artifact names never reach the new code path.