Repository Dependency Direction

Systemorph/MeshWeaver is the platform. MeshWeaver.Plugins and the four satellites consume it. The dependency runs one way, and every edge that runs the other way is a way core's own build or release can be broken by a repository it should not know about.

Plugins depending on core is CORRECT and must not be "fixed": $(MeshWeaverRoot) project references, the digest-pinned platform image the in-mesh gates compile against, and a plugin repo calling core's reusable node-repo-*.yml / plugin-build.yml workflows are all the intended shape. This page is only about the other direction.

Measured against Systemorph/MeshWeaver@cea28557b. Re-measure before acting on it; line numbers move. See also Carving Projects Out Of Core, which covers the moves that CREATE these edges.

The inventory

A · The image lanes build plugin source — structural, not a defect to delete

Memex.Portal.Distributed (the portal host) and Memex.Database.Migration (the migration worker) LIVE in MeshWeaver.Plugins since #2293. Core's image lanes therefore check that repo out and dotnet publish from it. Without the checkout there is no project to publish — this is not incidental coupling that can be tidied away.

Where What it does If Plugins is broken or absent
main-cd.yml:849 → :974 checks out Plugins @ vars.MW_PLUGINS_REF \|\| main, publishes plugins-repo/src/Memex.Portal.Distributed a Plugins compile error turns core CD red; no images publish for that core commit
main-cd.yml:1020 → :1080 same, for Memex.Database.Migration ditto — and a missing migration tag is the 6.5 h production outage of 2026-08-27 (#2555)
main-cd.yml:524 git ls-remote the Plugins HEAD, to key the image set on the PAIR (#2622) fails RED, deliberately: an unresolvable HEAD means the image identity cannot be established
main-cd.yml:2094 (plugins-modules), :2166 (plugins-bake) core's CD packs and bakes the Plugins module bundles the portals adopt for this identity a red leg means the sealed publication is missing for an identity the images already carry — and it stalls the whole fleet: memex still wakes every satellite for that identity, but each of their bakes seeds the Plugins publication and fails upstream 'plugins' has no SEALED publication until it exists
release.yml the v*.*.* tag lane checks out NO sibling: it promotes the set main-cd already built from the pair, so the Plugins commit a release carries is the one in that set's pair tag — (a release can only name a set that exists)
edge-images.yml:39 → :92, :105 the manual edge channel, same two projects edge builds fail

🚨 The blast radius is the FLEET, not this repo. A broken plugins tree does not merely red a core job. The release wave itself is memex's — the build fact still reaches Hosting/PlatformBuilds and memex still dispatches meshweaver-framework-released to every subscribed repository — but each satellite's bake seeds the Plugins publication for the released identity, so with plugins-bake red every one of them fails upstream 'plugins' has no SEALED publication and the fleet adopts nothing, reporting FrameworkDeclined at its next fetch. That is the cost of the edge, stated plainly.

🚨 ref: main is the dishonest part, not the checkout. A live checkout of a sibling's moving default branch means "which plugins commit shipped with which core commit" is decided by whoever merged last, and the same core commit can build differently on two runs. The variable MW_PLUGINS_REF makes that coupling steerable during an incident; it does not make it a pin.

The honest seams, in order of increasing cost:

  1. Pin, do not follow. Core records the plugins commit it builds against — a lockfile in core, or MW_PLUGINS_REF set to a sha rather than main — and a plugins release proposes a bump the same way a package upgrade does. The image set already keys on the PAIR (check-image-set.sh takes the plugins short-sha), so the identity machinery is in place; what is missing is that the pair is chosen rather than observed.
  2. Move the image build to the repo that owns the host. Plugins already builds and tests these projects; the lane that publishes them could live there and consume core as a released artifact. This inverts nothing else and removes every edge in the table.
  3. Move the hosts back to core. Correct by construction and the largest change; it also gives back the NuGet publication a source move costs (see the sibling page).

Until one of those lands, the coupling stands and should be stated, not discovered.

B · A gate whose subject is core, living in the plugin repo — a defect, fixed

DocumentationLinkIntegrityTest and DocumentationEmbedIntegrityTest pinned this repository's doc tree (src/MeshWeaver.Documentation/Data) from MeshWeaver.Plugins/src/MeshWeaver.AI.Test/, because they check the union of the Doc, Agent and Skill partitions and the latter two are embedded in MeshWeaver.AI, which ships from there.

The consequence: a core pull request that broke a doc link or a @@ embed went green here and turned a DIFFERENT, private repository red — hours later, on a change it did not make. A required gate sitting behind a sibling checkout is the sharpest form of an inverted dependency, because the repo that owns the subject has no way to run it.

The fix is a SPLIT, never a delete: core now carries the Doc-only half in test/MeshWeaver.Documentation.Test/, and the plugin repo keeps the union — the repo that holds BOTH assemblies validates cross-partition links, which is the correct direction. Nothing became unpinned; core simply stopped depending on someone else to notice.

🚨 A gate that moves and leaves its subject unpinned is not fixed, it is unpinned — with nothing turning red to say so. When a gate cannot move whole, split it along the assembly boundary and say in both copies what the other one covers.

C · A core PR gate that reads the fleet — deliberate, and worth naming

dotnet-test.yml:1768 (shared-rules) reads AGENTS.md from all seven repos and is a needs: of collect-results (:1955), this repo's only required status check. A shared-rule block that drifts in a satellite turns core red and blocks core merges until it is fixed there.

That is chosen rather than accidental, and the reasoning holds: a per-repo self-check cannot detect the missed-repo case, because a repo with no pull request never runs its own gate — "six of seven were updated" and "all seven were updated" produce identical evidence. The scheduled half (shared-rules.yml) exists for the weeks nobody opens a pull request here.

It is listed anyway, because anyone auditing "does core's build touch plugins?" must be able to find it rather than rediscover it.

dotnet-test.yml (cross-repo-pair) is the SECOND edge of this class, added for #2689. It resolves the pull request a surface-removing change declares (Pairs-with: Systemorph/MeshWeaver.Plugins#904) and refuses to let core merge until that counterpart is merged into its repo's default branch — see The Cross-Repo Pair Gate. It exists because a public type leaving core's src/ reddened MeshWeaver.Plugins' trunk for two hours on a change none of that repo's pull requests had made, and core's CI cannot see it: core does not build the plugin repos, so the coupling can surface nowhere except CD, after the fact.

🚨 The distinction that makes this class permitted while a checkout is not: a checkout puts another repository's SOURCE into core's build; an API read puts only a FACT about it into a verdict. Core still compiles, tests and ships with no sibling on disk. Every edge of this class is ASSERTED as well as documented — PlatformNeverDependsOnPluginsGuard.ApiReadLedger enumerates them and fails in both directions, so a new one has to be a decision rather than a diff nobody noticed.

The THIRD edge of this class is dotnet-test.yml's Dependent suites (MeshWeaver.Plugins) (policy dependent-suites-gate, #2689's acceptance criterion). Every merge-queue entry — and a pull request labelled dependent-suites — sends ONE repository_dispatch core-candidate-suites to MeshWeaver.Plugins naming the candidate commit, and then READS the verdict that repository writes at refs/core-candidate/<key> after building its reachable suites from source against the candidate. Plugins' source never enters core's build and Plugins writes nothing into core: the dispatch is a test REQUEST about an unmerged commit, and the read is a fact about it — see The Cross-Repo Pair Gate § "The dependent's suites run against the candidate".

🚨 This reverses a decision, and says so. A dispatcher of this shape (dependent-suites.yml, #3103) existed for a few hours on 2026-09-03 and was withdrawn by the maintainer — "none of the top-level repos should have any dependency to anyone else" — because it put a context on every core pull request that only MeshWeaver.Plugins could turn green, while Plugins had no receiver. Three core merges in two days (#5635, #5647, #5655) then turned Plugins' main red through behaviour changes no core gate could see, holding the sealed publication and every satellite for hours, and the maintainer asked for exactly this gate ("we don't want these total breakdowns"). What changed in the design: the receiver landed in Plugins FIRST and the request job proves it exists before sending; the Plugins side re-runs every candidate failure at the base, so a red that already exists there never blocks core; and the gate runs where landing happens (the merge queue), not on every push. The release wave is untouched — that stays memex's, and PlatformReleaseNotifyGuard.DispatchLedger admits exactly this one sender, this one event type and this one target. PlatformNeverDependsOnPluginsGuard.ApiReadLedger carries the read.

What is NOT an inverted edge

Checked and cleared — do not "fix" these:

The contract (maintainer, 2026-09-03: "end of github pipeline must call memex, which must register release and publish event") is three sentences:

  1. Every publishing pipeline ENDS with one call to memex. Core's CD, after the image set is promoted, POSTs the signed platform build (event: platform-build) into the control instance's Hosting/PlatformBuilds inbox (notify-platform-update). Every node repository's node-repo-publish-bake.yml run, after its bundles are sealed for an identity, POSTs the signed publication record (event: bundle-publication — source, identity, commit, tester + portal image) into the same inbox (register-publication, its last job). Nothing runs after that call, and no pipeline sends a repository_dispatch to another repository.
  2. memex REGISTERS the release as a durable node — Hosting/PlatformBuilds/<version> for a platform build, Hosting/Publications/<identity>/<source> for a bundle publication — the source of truth for "what is published for which identity" (what the self-update availability check reads).
  3. memex PUBLISHES the event from that registration: FrameworkReleaseBroadcaster sends meshweaver-framework-released (platform) or meshweaver-upstream-published (bundle publication, client_payload.version = the identity) to the subscribed repositories — the repositories the control instance's Hosting/Deployment records name as registry sources. The subscribers' CI receives it, resolves both images from the version, builds and publishes for that identity — and ends by calling memex (1).
 pipeline (core CD | a node repo's publish-bake)        memex (control instance)              subscriber CI
 ───────────────────────────────────────────────        ────────────────────────              ─────────────
 promote / seal ✅                                       WebhookInbox Hosting/PlatformBuilds
   └─ ONE signed POST ──(platform-build |──────────────▶│ verify HMAC
      bundle-publication)… and FINISH                    ├─ REGISTER  Hosting/PlatformBuilds/<version>
                                                         │            Hosting/Publications/<identity>/<source>
                                                         ├─ subscribers = Hosting/Deployment records'
                                                         │              pluginRepos[].isRegistrySource
                                                         └─ PUBLISH   repository_dispatch ─────────────▶ on: repository_dispatch:
                                                            meshweaver-framework-released |               types: [meshweaver-framework-released,
                                                            meshweaver-upstream-published                        meshweaver-upstream-published]
                                                                                                          → bake for the version → seal → POST memex

Where the pieces are: the POST steps in main-cd.yml and node-repo-publish-bake.yml (this repo); the inbox watcher, registration and broadcast in the Hosting module's PlatformBuildInboxWatcher (MeshWeaver.Plugins, Hosting/Deployment/Source); the broadcaster in src/MeshWeaver.GitSync. PlatformReleaseNotifyGuard.CoreDispatchesToNoRepository refuses a dispatch SENDER in any workflow under .github/workflows — there is no ledger — and UpstreamBuildGateGuard.TheLaneEndsByRegisteringWithMemex_AndDispatchesToNobody pins the lane's call.

The rule

A gate belongs in the repository that owns what it measures. When you move a project out of core, the gates that measure the code STAY with the code — but a gate that measures core content while merely needing a plugin assembly to do it belongs here, split along the assembly boundary. And when core genuinely must build from a plugin repo, it pins a commit; it does not follow a branch.