From 06baefceab19bb94a6ca0a2c6638512e9a7afcac Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 22 Apr 2026 12:59:48 -0700 Subject: [PATCH 1/3] chore: upgrade fallow gate to baseline-driven audit + Husky pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fallow 2.45 introduced stricter thresholds (CRAP >= 30 and updated cyclomatic/cognitive scoring) that flag 17 pre-existing complex functions plus 29 clone groups on untouched files (llm.ts chat/ recordOpenAICost, repository-claims.ts createClaim*, embedding.ts requestAndTrack, search-pipeline.ts applyExpansionAndReranking, memory-repository.ts ↔ pg-*-store.ts near-duplicates, etc.). Rather than pin fallow forever or rewrite the ingest/search/AUDN internals in one pass, switch the gate to baseline-driven `fallow audit`: - Save current state as the floor (.fallow/health-baseline.json and .fallow/dupes-baseline.json, tracked in git). CI compares against these; only NEW regressions beyond the baseline fail. - Husky v9 pre-commit hook runs `npx fallow audit` with the same baselines and the same auto-detected base-branch scope as CI, so a commit that passes locally will pass CI. - CI workflows switch from the pinned `npx fallow@2.43.0 --no-cache` to `npx fallow audit`. Unpinning lets future fallow improvements land; the baseline protects pre-existing debt from blocking unrelated PRs. .gitignore was split so baselines stay tracked while regenerable fallow artifacts (.fallow/cache.bin, .fallow/churn.bin) stay out of git. Follow-up worth tracking: - Extract the memory-repository ↔ pg-memory-store / pg-search-store near-duplicates (~375 lines across three clone groups) — biggest single win for the dupes baseline. - Dedupe the runtime-container vs formatHealthConfig config-snapshot reader (16 lines, already flagged). - Refactor the four HIGH-tagged functions (repository-write.ts buildBaseParams, repository-claims.ts createClaimVersionWithClient, llm.ts chat, memory-audn.ts tryOpinionIntercept) to drop the complexity ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) --- .fallow/dupes-baseline.json | 33 +++++++++++++++++++++++++++++++++ .fallow/health-baseline.json | 26 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 14 +++++++++----- .github/workflows/release.yml | 10 +++++----- .gitignore | 6 +++++- .husky/pre-commit | 11 +++++++++++ package-lock.json | 17 +++++++++++++++++ package.json | 4 +++- 8 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 .fallow/dupes-baseline.json create mode 100644 .fallow/health-baseline.json create mode 100755 .husky/pre-commit diff --git a/.fallow/dupes-baseline.json b/.fallow/dupes-baseline.json new file mode 100644 index 0000000..6e30449 --- /dev/null +++ b/.fallow/dupes-baseline.json @@ -0,0 +1,33 @@ +{ + "clone_groups": [ + "src/__tests__/memory-route-config-seam.test.ts:130-135|src/app/__tests__/composed-boot-parity.test.ts:117-122", + "src/__tests__/route-validation.test.ts:247-254|src/__tests__/route-validation.test.ts:79-86", + "src/__tests__/smoke.test.ts:11-24|src/app/__tests__/research-consumption-seams.test.ts:22-35", + "src/__tests__/smoke.test.ts:48-52|src/app/__tests__/research-consumption-seams.test.ts:57-61", + "src/app/__tests__/composed-boot-parity.test.ts:73-82|src/app/__tests__/composed-boot-parity.test.ts:89-101", + "src/app/runtime-container.ts:227-242|src/routes/memories.ts:611-626", + "src/db/memory-repository.ts:87-409|src/db/stores.ts:31-90", + "src/db/memory-repository.ts:124-145|src/db/pg-memory-store.ts:51-52", + "src/db/memory-repository.ts:157-165|src/db/pg-memory-store.ts:36-38", + "src/db/memory-repository.ts:197-213|src/db/pg-search-store.ts:26-42", + "src/db/memory-repository.ts:231-243|src/db/pg-memory-store.ts:40-41", + "src/db/memory-repository.ts:256-264|src/db/pg-memory-store.ts:41-42", + "src/db/memory-repository.ts:309-317|src/db/pg-representation-store.ts:25-27", + "src/db/memory-repository.ts:317-325|src/db/pg-representation-store.ts:23-25", + "src/db/memory-repository.ts:363-375|src/db/pg-search-store.ts:47-51", + "src/db/memory-repository.ts:379-403|src/db/pg-search-store.ts:55-62", + "src/db/repository-claims.ts:360-374|src/services/memory-lineage.ts:67-81", + "src/routes/memories.ts:144-149|src/routes/memories.ts:158-165", + "src/routes/memories.ts:465-476|src/routes/memories.ts:496-507", + "src/schemas/__tests__/agents.test.ts:18-26|src/schemas/__tests__/memories.test.ts:22-27", + "src/schemas/agents.ts:36-41|src/schemas/memories.ts:88-93", + "src/services/__tests__/ingest-fact-pipeline-workspace.test.ts:14-24|src/services/__tests__/memory-ingest-runtime-config.test.ts:51-63", + "src/services/__tests__/ingest-fact-pipeline-workspace.test.ts:22-27|src/services/__tests__/memory-ingest-runtime-config.test.ts:73-78|src/services/__tests__/search-pipeline-runtime-config.test.ts:72-77", + "src/services/__tests__/memory-ingest-runtime-config.test.ts:139-158|src/services/__tests__/memory-ingest-runtime-config.test.ts:178-194", + "src/services/__tests__/memory-ingest-runtime-config.test.ts:209-229|src/services/__tests__/memory-ingest-runtime-config.test.ts:251-271", + "src/services/__tests__/memory-service-config.test.ts:133-150|src/services/__tests__/memory-service-config.test.ts:174-191|src/services/__tests__/memory-service-config.test.ts:97-114", + "src/services/ingest-fact-pipeline.ts:167-170|src/services/ingest-fact-pipeline.ts:83-87", + "src/services/ingest-fact-pipeline.ts:127-131|src/services/ingest-fact-pipeline.ts:165-172", + "src/services/memory-lineage.ts:284-291|src/services/memory-lineage.ts:319-326" + ] +} \ No newline at end of file diff --git a/.fallow/health-baseline.json b/.fallow/health-baseline.json new file mode 100644 index 0000000..87f663e --- /dev/null +++ b/.fallow/health-baseline.json @@ -0,0 +1,26 @@ +{ + "findings": [ + "src/db/repository-write.ts:buildBaseParams:140", + "src/db/repository-claims.ts:createClaimVersionWithClient:256", + "src/services/llm.ts:chat:176", + "src/services/memory-audn.ts:tryOpinionIntercept:132", + "src/services/search-pipeline.ts:applyExpansionAndReranking:667", + "src/services/extraction.ts:repairTruncatedJson:38", + "src/services/llm.ts:cleaned:78", + "src/services/extraction-enrichment.ts:inferCrossEntityRelations:205", + "src/services/deferred-audn.ts:applyDeferredDecision:196", + "src/services/llm.ts:recordOpenAICost:147", + "src/services/llm.ts:chat:233", + "src/services/quick-extraction.ts:extractFactBearingTurns:82", + "src/db/repository-claims.ts:createClaimWithClient:58", + "src/services/embedding.ts:requestAndTrack:99", + "src/services/supplemental-extraction.ts:shouldIncludeSupplementalFact:41", + "src/services/__tests__/poisoning-dataset.ts:generateLegitimateVariations:286", + "src/services/atomicmem-uri.ts:resolve:33" + ], + "production_coverage_findings": [], + "target_keys": [ + "src/services/conflict-policy.ts:high impact", + "src/services/composite-dedup.ts:high impact" + ] +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91f119e..1f3c5a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,11 +56,15 @@ jobs: # lands with schemas changed but spec not regenerated. run: npm run check:openapi - - name: Code health (fallow) - # Pinned to the last version main's CI passed cleanly with. Newer - # versions (2.45.0) flag pre-existing complexity on untouched files. - # Bump deliberately alongside a pass to address flagged issues. - run: npx fallow@2.43.0 --no-cache + - name: Code health (fallow audit vs baselines) + # Audit changed files against frozen baselines in .fallow/. New + # complexity / duplication regressions fail; pre-existing + # baseline-level debt is grandfathered. Refactor + regenerate the + # baseline to lower the floor. See .fallow/README.md if added. + run: npx fallow audit + --health-baseline=.fallow/health-baseline.json + --dupes-baseline=.fallow/dupes-baseline.json + --no-cache - name: Run tests run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a82e66..bf42361 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,11 +45,11 @@ jobs: - name: Type check run: npx tsc --noEmit - - name: Code health (fallow) - # Pinned to the last version main's CI passed cleanly with. Newer - # versions (2.45.0) flag pre-existing complexity on untouched files. - # Bump deliberately alongside a pass to address flagged issues. - run: npx fallow@2.43.0 --no-cache + - name: Code health (fallow audit vs baselines) + run: npx fallow audit + --health-baseline=.fallow/health-baseline.json + --dupes-baseline=.fallow/dupes-baseline.json + --no-cache - name: Run tests run: npm test diff --git a/.gitignore b/.gitignore index a51269e..de342b8 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,11 @@ coverage/ .nyc_output/ # Code analysis tools -.fallow/ +# Baselines (health-baseline.json, dupes-baseline.json) ARE tracked so CI +# and pre-commit hooks share the same floor; only the incremental cache +# and churn snapshot are regenerable and ignored. +.fallow/cache.bin +.fallow/churn.bin .brv/ .perf-baselines/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..3458886 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# Fallow gate: block commits that introduce new complexity or duplication +# beyond the frozen baselines in .fallow/. Audit auto-detects the base +# branch (typically main) and scopes to files changed on this branch — +# the same semantics CI uses, so a commit that passes here will pass CI. +# To lower a baseline, refactor the flagged code and regenerate: +# npx fallow health --save-baseline=.fallow/health-baseline.json +# npx fallow dupes --save-baseline=.fallow/dupes-baseline.json +npx fallow audit \ + --health-baseline=.fallow/health-baseline.json \ + --dupes-baseline=.fallow/dupes-baseline.json diff --git a/package-lock.json b/package-lock.json index 114b897..6fe822b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/node": "^22.0.0", "@types/pg": "^8.15.0", "dotenv-cli": "^8.0.0", + "husky": "^9.1.7", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^4.1.3", @@ -2587,6 +2588,22 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", diff --git a/package.json b/package.json index 6053e2e..badbf17 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "test:deployment": "vitest run src/__tests__/deployment-config.test.ts --reporter verbose", "test:docker-smoke": "./scripts/docker-smoke-test.sh", "migrate": "dotenv -e .env -- tsx src/db/migrate.ts", - "migrate:test": "dotenv -e .env.test -- tsx src/db/migrate.ts" + "migrate:test": "dotenv -e .env.test -- tsx src/db/migrate.ts", + "prepare": "husky" }, "dependencies": { "@anthropic-ai/sdk": "^0.80.0", @@ -86,6 +87,7 @@ "@types/node": "^22.0.0", "@types/pg": "^8.15.0", "dotenv-cli": "^8.0.0", + "husky": "^9.1.7", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^4.1.3", From 828b11dc05c356833c5dea0d642d6a3738e61960 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 22 Apr 2026 13:03:31 -0700 Subject: [PATCH 2/3] ci: fetch full history + pass --base to fallow audit actions/checkout@v4 defaults to fetch-depth: 1, leaving only HEAD in the runner's clone. fallow audit then fails with "could not detect base branch" because it has no ref to diff against. Fix: fetch-depth: 0 and pass --base=origin/ explicitly so audit scopes to the PR's diff against its target branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/release.yml | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f3c5a5..975b38a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,11 @@ jobs: steps: - uses: actions/checkout@v4 + with: + # Full history so `fallow audit` can compute `--base main ... HEAD` + # against a real ref. Default fetch-depth: 1 leaves only HEAD and + # audit fails with "could not detect base branch". + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -64,6 +69,7 @@ jobs: run: npx fallow audit --health-baseline=.fallow/health-baseline.json --dupes-baseline=.fallow/dupes-baseline.json + --base=origin/${{ github.base_ref || github.event.repository.default_branch }} --no-cache - name: Run tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf42361..e09ab64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + # Full history so `fallow audit` can walk back to main. + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -46,9 +49,13 @@ jobs: run: npx tsc --noEmit - name: Code health (fallow audit vs baselines) + # Tag pushes: compare against main so the release's diff is + # scoped the same way PRs are. `origin/main` is available + # because fetch-depth: 0 pulled the full history. run: npx fallow audit --health-baseline=.fallow/health-baseline.json --dupes-baseline=.fallow/dupes-baseline.json + --base=origin/main --no-cache - name: Run tests From dce7464ca25a4392a0851f5177dc65d91e723f31 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 22 Apr 2026 13:12:24 -0700 Subject: [PATCH 3/3] ci: add shrink-only baseline ratchet + seed tech-debt.md Ratchet: new step runs scripts/check-baseline-ratchet.sh after the fallow audit, failing any PR that grows .fallow/health-baseline.json or .fallow/dupes-baseline.json beyond the base branch's entry count. Refactors that drop entries pass freely; regressions are blocked even if they'd still fit under the grandfathered audit. Over time, this pushes the baseline to zero without anyone having to remember. Seeded tech-debt.md with the 17 HIGH-complexity functions and 29 clone groups the current baseline grandfathers, each with enough context to pick up later. Lists the high-leverage wins first (memory-repository dedupe, the four HIGH-tagged service functions, the repository-claims / memory-lineage clone) so the next debt-paydown pass knows where to start. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 10 +++++- scripts/check-baseline-ratchet.sh | 50 ++++++++++++++++++++++++++++ tech-debt.md | 55 +++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100755 scripts/check-baseline-ratchet.sh create mode 100644 tech-debt.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 975b38a..dc98187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,12 +65,20 @@ jobs: # Audit changed files against frozen baselines in .fallow/. New # complexity / duplication regressions fail; pre-existing # baseline-level debt is grandfathered. Refactor + regenerate the - # baseline to lower the floor. See .fallow/README.md if added. + # baseline to lower the floor — the ratchet step below enforces + # monotonic improvement. run: npx fallow audit --health-baseline=.fallow/health-baseline.json --dupes-baseline=.fallow/dupes-baseline.json --base=origin/${{ github.base_ref || github.event.repository.default_branch }} --no-cache + - name: Baseline ratchet (shrink-only) + # Blocks PRs that grow .fallow/*-baseline.json. Refactors that + # drop entries pass freely; new entries fail even if they'd be + # inside the grandfathered audit. Ensures debt goes monotonically + # to zero over time. + run: ./scripts/check-baseline-ratchet.sh origin/${{ github.base_ref || github.event.repository.default_branch }} + - name: Run tests run: npm test diff --git a/scripts/check-baseline-ratchet.sh b/scripts/check-baseline-ratchet.sh new file mode 100755 index 0000000..bf730dc --- /dev/null +++ b/scripts/check-baseline-ratchet.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Enforces monotonic improvement on fallow baselines: CI fails if a PR +# grows .fallow/health-baseline.json or .fallow/dupes-baseline.json +# beyond what's on the base branch. Refactors that shrink the baseline +# are celebrated; regressions are blocked. +# +# Runs in CI after the fallow audit step. Requires `jq` and a full +# git history (fetch-depth: 0 in the workflow's checkout step). +set -euo pipefail + +BASE_REF="${1:-origin/main}" + +check_baseline() { + local file="$1" + local json_path="$2" + local label="$3" + + if [ ! -f "$file" ]; then + echo "$label: baseline missing at $file; skipping" + return 0 + fi + + # If the baseline file is brand new on this PR (not tracked on the base + # branch), there's nothing to ratchet against — the PR IS the floor. + if ! git cat-file -e "$BASE_REF:$file" 2>/dev/null; then + echo "$label: new baseline on this PR; skipping ratchet check" + return 0 + fi + + local base_count head_count + base_count=$(git show "$BASE_REF:$file" | jq "$json_path | length") + head_count=$(jq "$json_path | length" "$file") + + if [ "$head_count" -gt "$base_count" ]; then + echo "::error file=$file::baseline grew from $base_count to $head_count entries. Baselines ratchet: refactor the flagged code and regenerate the baseline, do not add new entries." + return 1 + fi + + local delta=$((base_count - head_count)) + if [ "$delta" -gt 0 ]; then + echo "$label: $base_count -> $head_count (-$delta) ✓ shrunk" + else + echo "$label: $base_count -> $head_count (unchanged)" + fi +} + +fail=0 +check_baseline .fallow/health-baseline.json '.findings' health || fail=1 +check_baseline .fallow/dupes-baseline.json '.clone_groups' dupes || fail=1 +exit "$fail" diff --git a/tech-debt.md b/tech-debt.md new file mode 100644 index 0000000..ea0784b --- /dev/null +++ b/tech-debt.md @@ -0,0 +1,55 @@ +# Tech debt + +Items deferred rather than fixed inline. Fallow baselines at +`.fallow/health-baseline.json` and `.fallow/dupes-baseline.json` keep +these from blocking PRs; the CI ratchet check keeps the baseline +monotonic (shrink-only), so every refactor that lowers an entry counts. + +When fixing an item: refactor the code, regenerate the relevant +baseline (`npx fallow health --save-baseline=.fallow/health-baseline.json` +or `npx fallow dupes --save-baseline=.fallow/dupes-baseline.json`), +commit both the code change and the smaller baseline, and delete the +entry from this file. + +## HIGH-complexity functions (baseline: 4 HIGH + 13 other, 17 total) + +Each entry blocks the ceiling; splitting any of them lets the baseline +shrink. + +- [ ] `src/db/repository-write.ts:140 buildBaseParams` — 15 cyclomatic / 14 cognitive / 26 lines / CRAP 63.6. Param-building branch tree; candidate for a config-table pattern. +- [ ] `src/db/repository-claims.ts:256 createClaimVersionWithClient` — 15 / 9 / 46. Multi-insert transaction; extract the slot-resolution and version-linking branches. +- [ ] `src/services/llm.ts:176 chat` — 14 / 13 / 40. Provider-dispatch plus retry; split the retry wrapper from the provider selection. +- [ ] `src/services/memory-audn.ts:132 tryOpinionIntercept` — 14 / 8 / 28. Confidence-threshold cascade; table-drive the decision. +- [ ] `src/services/search-pipeline.ts:667 applyExpansionAndReranking` — 13 / 12 / 100 lines. The longest function in core; split into expansion / reranking / packaging phases. +- [ ] `src/services/extraction.ts:38 repairTruncatedJson` — 11 / 10 / 38. +- [ ] `src/services/llm.ts:78 cleaned` — 11 / 9 / 11. +- [ ] `src/services/extraction-enrichment.ts:205 inferCrossEntityRelations` — 11 / 14 / 20. +- [ ] `src/services/deferred-audn.ts:196 applyDeferredDecision` — 11 / 7 / 49. +- [ ] `src/services/llm.ts:147 recordOpenAICost` — 10 / 6 / 18. +- [ ] `src/services/llm.ts:233 chat` (second `chat`) — 10 / 6 / 23. Same file as the HIGH one above; may dedupe via shared base. +- [ ] `src/services/quick-extraction.ts:82 extractFactBearingTurns` — 10 / 11 / 40. +- [ ] `src/db/repository-claims.ts:58 createClaimWithClient` — 10 / 5 / 25. +- [ ] `src/services/embedding.ts:99 requestAndTrack` — 10 / 5 / 13. +- [ ] `src/services/supplemental-extraction.ts:41 shouldIncludeSupplementalFact` — 10 / 6 / 35. +- [ ] `src/services/__tests__/poisoning-dataset.ts` — 10 / 9 / 112 (test fixture; may be fine as-is). +- [ ] `src/services/atomicmem-uri.ts:33 resolve` — 10 / 13 / 34. + +## Clone groups (baseline: 29 groups, 848 lines) + +Biggest wins come from extracting shared helpers across the storage +adapter / DB layer. + +- [ ] **memory-repository ↔ pg-*-store near-duplicates** (~375 lines across 3 clone groups). `src/db/memory-repository.ts:87-409` vs `src/db/stores.ts:31-90`, and several smaller blocks against `pg-memory-store.ts` and `pg-search-store.ts`. Extract a shared DB-access helper into `src/db/`. Biggest single win. +- [ ] **runtime-container ↔ formatHealthConfig** (16 lines). `src/app/runtime-container.ts:227-242` vs `src/routes/memories.ts:611-626`. Both snapshot the same runtime config fields; extract a single `snapshotRuntimeConfig` helper. +- [ ] **repository-claims ↔ memory-lineage** (15 lines). `src/db/repository-claims.ts:360-374` vs `src/services/memory-lineage.ts:67-81`. +- [ ] **test setup duplication**. `memory-ingest-runtime-config.test.ts` has 41 lines of internal dupes across 2 groups; `memory-service-config.test.ts` has 18 lines across 3 instances; `smoke.test.ts` + `research-consumption-seams.test.ts` share 14 lines of bootstrap. Extract to test-fixtures. +- [ ] 20 remaining smaller clone groups — run `npx fallow dupes` for the full list. + +## OpenAPI response schemas (separate from fallow) + +- [ ] Response bodies aren't declared in the OpenAPI spec today — only request bodies. Adding them would let `npm run check:openapi` catch wire-shape drift automatically (the earlier snake_case flip couldn't have silently changed the spec). See `atomicmemory-core/src/schemas/openapi.ts`. + +## Fallow version pin removal + +Landed in the baseline-upgrade PR. No action required — CI now uses +fallow's latest via `npx fallow audit`.