# Bean & Bark: a teammate joins

Monday's post-mortem verdict: the pricing bug happened because pricing logic lives in **two places** — the API and Tunde's storefront each carry a copy. The fix is one shared `BeanAndBark.Pricing` library in the platform repo. Which raises the question that has broken a thousand onboarding days: how does *this* repo build against code that lives in *that* repo — on your machine, on Tunde's, and on a CI runner that has neither?

**You'll learn**

- Declare a cross-repo dependency by what it *is* — git URL, ref, project path — with `kanject add lib`
- Read what `kanject sync` writes: the lockfile and the generated MSBuild imports
- Explain why a fresh clone builds with `sync` + `dotnet build` and nothing else
- Keep CI honest with `sync --locked`, and know what KANCLI196 is telling you

> **Monday, 9:40am — the ticket:** **Nadia**: *"welcome Tunde to the repo!! 🎉 also: pricing logic is moving to the platform repo so friday can never happen twice. make the API use the shared library — and Tunde needs to be building today, not archaeology-ing our laptops."*

## The trap you're avoiding

The reflex solution is a `<ProjectReference Include="..\..\platform\src\BeanAndBark.Pricing\…" />` — and it works perfectly on the one machine that happens to have `platform` checked out at exactly that relative path. Tunde's laptop doesn't. The CI runner *definitely* doesn't: its clean checkout fails restore with an "unable to find project" error, and the build is red before lunch. A path on someone's disk is not a dependency declaration.

## Declare what it is, not where it sits

A cross-repo dependency has three identifying facts: the repo, the ref, and the project inside it — plus the consumer in *this* repo that uses it:

```bash
kanject add lib \
  --repository git@github.com:beanandbark/platform.git \
  --ref main \
  --project src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj \
  --consumer src/BeanAndBark.Orders/BeanAndBark.Orders.csproj
```

That appends one entry to `manifest.json` and immediately runs `sync` (pass `--no-sync` to defer). Notice what it does **not** do: your consumer `.csproj` is never edited — the wiring arrives another way.

```json
"dependencies": [
  {
    "name": "BeanAndBark.Pricing",
    "repository": "git@github.com:beanandbark/platform.git",
    "ref": "main",
    "projectPath": "src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj",
    "consumers": ["src/BeanAndBark.Orders/BeanAndBark.Orders.csproj"]
  }
]
```

## What sync actually does

```text
Sync  generated MSBuild imports ────────────────────────────────

  ✓  Synced 1 unique dep(s) across 1 consumer edge(s). 1 dep changed.
  Lock:  kanject-cli/manifest.lock.json  (sha256:4be1a9c02f…)
  Wired: kanject-cli/kanject.g.props + .g.targets
```

Three lines, three artifacts. Sync resolved `main` to a commit SHA (`git ls-remote`), cloned the platform repo *at that exact SHA* into the git-ignored `.kanject/cache/`, and wrote two things: **`manifest.lock.json`** — the ref pinned to its resolved commit, with content hashes — and **`kanject.g.props` + `kanject.g.targets`**, generated MSBuild imports that wire the dependency into your build. Your csproj stays untouched; delete `.kanject/` any time and re-sync.

Commit the lockfile and **review its diffs**: a changed SHA in `manifest.lock.json` *is* a dependency upgrade, and it deserves the same eyes as any other change in the PR.

## Tunde's first build

```bash
# Tunde, day one: clone, resolve, build. No sibling checkouts, no wiki page.
git clone git@github.com:beanandbark/orders-api.git && cd orders-api
kanject sync
dotnet build
```

One guess before he runs it: which commit of the pricing library does Tunde get — whatever `main` points at this morning, or the one you built against on Friday? **Yours.** Sync replays the lock: same SHA, same content hashes, byte-identical build, even though `main` has moved since. And after this first sync, the git hooks kanject installed keep him current automatically — every `git pull` that brings a manifest change re-syncs without a keystroke (run `kanject msbuild status` any time to inspect the wiring).

## CI: verify, never drift

```bash
# CI: verify — never silently upgrade
kanject sync --locked
```

On a runner, `--locked` changes sync's job from *resolve* to *verify*: if the manifest and lockfile disagree — someone edited a `ref` and forgot to re-sync — it fails fast with **KANCLI196** instead of silently building something nobody reviewed. A surprise dependency upgrade in CI is exactly the class of Friday you've just lived through. (The AWS pipeline from the CLI docs runs `sync` before `dotnet build` for precisely this reason; `--offline` goes further and forbids the network entirely, trusting lock + cache.)

> **A branch ref is a moving target:** Leaving `ref: "main"` means every deliberate re-sync on a dev machine re-resolves to whatever `main` is *that day*. Fine for a library changing in lock-step with you — but pin anything prod-critical to a **tag or commit SHA**, so an upgrade is always an explicit, reviewable lockfile diff and never a side effect of syncing.

> **🎯 Building before the second coffee:** 10:20am: Tunde's clean clone builds in three commands, against the same pricing bytes as yours, with zero tribal knowledge transferred. There is one copy of the pricing logic, one pinned SHA in a reviewed lockfile, and a CI runner that will loudly refuse to build anything else. Friday can no longer happen by accident — someone would have to *merge* it.

**Recap**

- Declare cross-repo dependencies by identity — repo, ref, project path, consumers — via `kanject add lib` into `manifest.json`; consumer csprojs are never edited.
- `sync` resolves the ref to a SHA, clones into the disposable `.kanject/` cache, and writes the lockfile plus generated MSBuild imports (`kanject.g.props` / `.g.targets`).
- Commit `manifest.lock.json` and review its diffs — a changed SHA is a dependency upgrade. A fresh clone needs only `sync` + `dotnet build`.
- `sync --locked` in CI fails fast (KANCLI196) on manifest/lock drift; pin prod-critical deps to a tag or SHA, not a branch.

**Before Tunde's first PR — check yourself**

**1. You pull a teammate's change that adds a new dependency to the manifest. What wires it into your build?**

- The git hook — post-merge runs sync automatically ✓ — kanject installs post-merge and post-checkout hooks, so a pull that changes the manifest re-syncs without a keystroke. Manual `kanject sync` is the fallback, not the routine.
- `dotnet restore` fetches it from NuGet — the dependency isn't on NuGet — it's a git repo. Restore only sees it after sync has cloned, pinned, and wired it through the generated imports.
- You must re-run `kanject add lib` yourself — `add lib` *declares* a dependency once. Resolving an already-declared one is sync's job — and the hooks run it for you.

**2. CI fails with KANCLI196 on `sync --locked`. What happened?**

- The manifest and lockfile disagree — a dep changed without a re-sync ✓ — that's locked-mode drift: someone edited a `ref` (or added a dep) and didn't commit a re-synced lockfile. Fix locally — `kanject sync`, review the lock diff, commit — and CI goes green honestly.
- The dependency repo is unreachable from CI — clone failures surface as git/credential errors, not KANCLI196. Locked-mode drift is detected *before* any network call — it's a file comparison.
- The lockfile is corrupted and must be deleted — never hand-delete the lock to make CI pass — that discards the pin everyone else builds against. Drift means *reconcile*, not *erase*.

**Try it yourself**

**1. Pin pricing to a release**

The platform team tags `pricing-v2.1.0`. Move the API off the moving `main` ref and onto the tag, so future upgrades are always explicit.

_Hint:_ Edit the `ref` in `manifest.json → dependencies[]`, then let sync re-pin the lock. Watch what changes in `manifest.lock.json`.

_Solution:_ Change `"ref": "main"` to `"ref": "pricing-v2.1.0"` and run `kanject sync`. The lockfile diff shows the newly resolved SHA — commit both files together. From here, upgrading pricing is a one-line manifest change with a reviewable lock diff, never a surprise.

```bash
kanject sync
```

**2. Explain the red runner**

A teammate's PR edits a dependency's `ref` but the diff touches only `manifest.json`. Predict exactly how this PR dies in CI — which command, which code, and what the author should have done.

_Hint:_ What does locked-mode sync compare, and when does it run relative to the build?

_Solution:_ `kanject sync --locked` runs before the build, compares manifest against lockfile, finds the edited ref unpinned, and fails fast with KANCLI196 — before a single line compiles. The author should have run `kanject sync` locally and committed the updated `manifest.lock.json` in the same PR, making the upgrade visible in review.

> **Where next?:** The source is shared, but the storefront still cannot install a supported version of it. [Release the Pricing SDK](https://www.kanject.com/docs/cli-bean-bark-packages/) turns that cross-repo project into a governed package: public/private closure, immutable versions, a candidate Tunde can test, and an approved journaled release. The full source-dependency reference stays at [Dependency Sources & Resolution](https://www.kanject.com/docs/cli-dependencies/).

---
_Source: https://www.kanject.com/docs/cli-bean-bark-teammate/ · Kanject Docs_
