Skip to content

fix(gate,review,ops): add glob exclusions, tier the public-comment vocabulary, and make inert config queryable (#9554, #9555, #9433) - #9556

Merged
JSONbored merged 4 commits into
mainfrom
fix/glob-exclusions-and-inert-config
Jul 28, 2026
Merged

fix(gate,review,ops): add glob exclusions, tier the public-comment vocabulary, and make inert config queryable (#9554, #9555, #9433)#9556
JSONbored merged 4 commits into
mainfrom
fix/glob-exclusions-and-inert-config

Conversation

@JSONbored

@JSONbored JSONbored commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Three defects that share one shape: a safety mechanism that is correct in aggregate but too blunt at the edges, silently destroying good content or hiding its own inertness.

Closes #9554
Closes #9555
Closes #9433

#9554 — the path matcher cannot exclude, so a gate auto-closed PRs for a JSON file

matchesAny is positive-only: no ! negation, and {a,b} braces compile to literals. So apps/loopover-ui/public/** could not carve out the generated openapi.json inside it — and CLAUDE.md requires contributors to regenerate that file on any API change. Following the documented contribution steps tripped a gate demanding a Desktop/Tablet/Mobile × Dark before/after screenshot matrix of a JSON spec, and with action: close being one-shot, 5 contributor PRs were auto-closed with no recovery path.

matchesAnyWithExclusions is additivematchesAny is unchanged, because it is also the hard-guardrail matcher where a maintainer's literal path starting with ! must stay guarded, not be silently reinterpreted.

The load-bearing subtlety is that the two halves need opposite fail directions for an over-complex (ReDoS-capped) glob. An unsafe include fails toward matching — safe, more paths in scope, and matchesAny's existing deliberate behaviour. An unsafe exclude must fail toward excluding nothing: failing it toward "matches everything" would silently widen the exclusion and shrink gate coverage, the opposite of what a safety gate is for. My first implementation reused matchesAny for both halves and got this backwards; the security test caught it.

Sibling repos, verified on the server (parent issue's follow-up 1). metagraphed and awesome-claude both carried the identical over-broad glob — so this was never loopover-specific and both were live-exposed with action: close. Narrowed both to the actually-rendered UI, matching the fix already proven on loopover, with a comment recording why and backups taken first. No restart needed: private-config.ts reads these fresh per lookup.

#9555 — ordinary English still drops narrative sentences

#9445 fixed the catastrophic half of #9432 (per-sentence filtering instead of all-or-nothing). The residual: FORBIDDEN_PUBLIC_COMMENT_WORDS is matched with a plain .includes() and contains ordinary English — reward, ranking, cohort, farming, reviewability. Only bare score had a per-repo escape hatch, so on every repo a sentence like "this improves reviewability and updates the ranking comparator" was still dropped. No longer a missing summary — a quietly incomplete one, which is harder to notice.

Split into ALWAYS_FORBIDDEN_PUBLIC_COMMENT_WORDS (never exemptible) and AMBIGUOUS_PUBLIC_COMMENT_WORDS (exemptible via the existing allowlist — no new config surface).

Why the split is safe: the risk is a leaked private value, and a value is never a bare noun — it is qualified (reward estimate 12 TAO, raw trust score 0.82, score preview). Every qualified form stays in the always-forbidden tier, enforced for every repo. A bare reward with no number leaks nothing. This generalizes the judgment allowBareScoreTerm already made for score rather than inventing a new one.

#9433 — a report, not more boot warnings

The confirmed instance (an unset score-terms allowlist silently replacing every narrative containing "score") sat live for weeks with a green suite, because tests verify a mechanism when enabled, never that an operator set the variable.

I did not follow the issue's obvious path of adding one boot warning per var, because verifying first showed it would be actively harmful. LOOPOVER_PUBLIC_STATS_REPOS is unset on the ORB self-host box and that is correct — the public-stats surface is served by the Cloudflare Worker, where wrangler.jsonc sets it to four repos (confirmed live: byProject returns 3 entries). An unconditional warning would fire on every self-host deployment forever for a non-problem — the same alert fatigue that let a genuinely broken backup alert be ignored for 8 days.

What is actually missing is answerability: no way to ask "which config-gated behaviours are inert on this box?" without reading long-scrolled boot logs. loopover_inert_config answers exactly that, on demand, with zero steady-state noise and cardinality bounded by construction (the key set is a fixed list in code). Every entry is classified deployment-specific precisely so nothing here is auto-escalated to a warning.

Scope is deliberately narrow: only vars whose unset state changes output correctness. The ~100 Default OFF convergence flags are excluded — listing them would bury the few that matter.

Validation

  • npx tsc --noEmit, selfhost:env-reference:check (169 vars), dead-source-files:check, actionlint, git diff --check — all clean
  • 558 passed across guardrail, screenshot-gate, queue-intelligence, inert-config, metrics, ai-review and ai-summaries
  • Patch coverage: 0 uncovered changed lines (the one reported line is in src/server.ts, which is explicitly Codecov-ignored as boot wiring covered by the Docker boot smoke test)

Regressions: the exclusion carves a generated file out of a matching directory glob, in both places that read whenPaths; an allowlisted repo keeps a sentence using each ambiguous term; the report names the confirmed #9433 instance.

Invariants: with no exclusions, behaviour is identical to matchesAny; an all-exclude list is not an implicit wildcard include; a bare ! stays a literal include. SECURITY, asserted exhaustively over the real arrays so a term in the wrong tier fails immediately: every always-forbidden phrase still throws with the exemption on; a qualified private value stays blocked even though its bare noun is exempt; an exclude glob is bound by the same ReDoS cap and fails toward excluding nothing. The two tiers are disjoint and together exactly equal the combined list. Gauge samples and entries stay in lockstep, so three surfaces read one answer rather than three drifting lists.

Closes #9560

@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 90513c7 Commit Preview URL

Branch Preview URL
Jul 28 2026, 09:41 AM

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.69231% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.62%. Comparing base (968c731) to head (90513c7).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...oopover-engine/src/review/screenshot-table-gate.ts 33.33% 6 Missing ⚠️
...es/loopover-engine/src/signals/change-guardrail.ts 95.12% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9556      +/-   ##
==========================================
- Coverage   89.62%   89.62%   -0.01%     
==========================================
  Files         867      868       +1     
  Lines      110801   110861      +60     
  Branches    26346    26355       +9     
==========================================
+ Hits        99307    99355      +48     
- Misses      10229    10237       +8     
- Partials     1265     1269       +4     
Flag Coverage Δ
backend 95.32% <100.00%> (-0.01%) ⬇️
engine 65.79% <60.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/queue-intelligence.ts 100.00% <100.00%> (ø)
src/selfhost/inert-config.ts 100.00% <100.00%> (ø)
src/selfhost/metrics.ts 100.00% <ø> (ø)
...es/loopover-engine/src/signals/change-guardrail.ts 97.36% <95.12%> (-0.50%) ⬇️
...oopover-engine/src/review/screenshot-table-gate.ts 69.85% <33.33%> (-0.91%) ⬇️

... and 1 file with indirect coverage changes

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 210 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.66MB 210 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-CYbNOnEV.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-BsErCrLz.js (New) 865.99kB 865.99kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-BkBDEKKn.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-CxTEgKqL.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BvuP0842.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-CWA8Dz3J.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/self-hosting-configuration-GO9aG0jf.js (New) 102.29kB 102.29kB 100.0% 🚀
assets/maintainer-panel-C9A52DqO.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-ChWzi9jO.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-KQjjR-qB.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-CU0RVvq3.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-CiHqZdx2.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-zVh86H-x.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-DTeORplD.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-CYFHqa3X.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-CXX-MeUM.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-D2lZRmDP.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-BZgaH6Fn.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-RIGQh-lF.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-CQj1083V.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-7TnvYiB2.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-ippVh3Rk.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-DZ2M_rKC.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-B-rMxOua.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-BHE4Dykx.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-BzHjvhZf.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-VrcC8sLb.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-p6sYIACO.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-D25tLdpt.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-cKCECan_.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-BNCAzI-M.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BvW1pryo.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-BBCWgGvI.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-Csc2ZXGf.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-D4OBK90G.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-DXLIrxat.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-DFSJx3CQ.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-CSST0zel.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-ifCg_i-4.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog--tq5fJQE.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-DyzBBi-o.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-CSoKFq9Q.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BI2cMhD5.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BtMUETVn.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-D41Z91lm.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-NpEADLZY.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-ePXc5djW.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-Bj3LSN04.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-k3MkIdYu.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-CmRLSQ-t.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-BJKhTP0s.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-Bpl7X6ye.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-C4y8z_Fy.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-CDy3kTwz.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-C1UB-JHW.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-aanmfgPj.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-Wwhx1Zac.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-SjTvHtWN.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-DqkSAjRd.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BSjUoCsC.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-B3jz2g3l.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-C3VG5bua.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-DXx-OJV2.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-Dxry2-5g.js (Deleted) -865.99kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-nFpMT86X.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-Ivw5ax1r.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-C8SIET1q.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-Cok72R5D.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/self-hosting-configuration-1r6dwu38.js (Deleted) -102.08kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-D_lyIwB7.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-Ck-nXnZY.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-SihDsaoF.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-O7EW3g8j.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-Db3arImx.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-X2WhlK_K.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-DyQQpFdZ.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-BZBJhBja.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-iS-ODV77.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-CyHTj6MS.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-B7Ce82Co.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness--Yqh4-4p.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-iTsUKuwR.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-BX3u5Zp8.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-Dok9r-H2.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-CWt9O0I9.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-kpAU_a9u.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-BZJA1eHZ.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-B1oiVdEd.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-DdC5Sm6K.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BSwjd6-6.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-BgIUu1Cg.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-D-VlqyPO.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-4rZA0J7x.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-9VAVNjiU.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-CNptTrcV.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-DqvPtMJM.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-Bon3heh4.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CBBhvGQn.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BAi8KLrb.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-DBQnh09S.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-sTy7xHqw.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-BQuvdOOV.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-BsKyAbBW.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-CFB9JjZC.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-CMqPMji6.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-DZosXb-e.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-jkJ1cunD.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-DeSixYs1.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-B1mHHCOO.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-zzV4P12o.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-DOq5dzPq.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BVdpZtBJ.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-FgSMP7vT.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-B-UXNlP6.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-BHwyy9Kj.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-DdVyaHp-.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-DhCkEBeo.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-Bdantkc5.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-D1PDCT3t.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-CtXf0993.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-Hc7GzJaU.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-CvTH65rn.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CMgUP_o1.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-Dp2iu1K7.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@loopover-orb loopover-orb Bot added gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context labels Jul 28, 2026
@JSONbored

Copy link
Copy Markdown
Owner Author

Pushed additional coverage for the two engine files Codecov flagged.

Diagnosis. Codecov ran on my current head (29ed46a), so the report was real rather than stale. Digging into the v8 report, my three actual code lines in screenshot-table-gate.ts were all hit (20 hits each) and v8 emits no entries for comment lines — lines 177–181 are simply absent from the statement map. So the shortfall was not uncovered logic.

The likely cause is shard attribution: packages/loopover-engine/src/** and its src/** re-export shim are two distinct coverage identities, and CI shards the suites — so a shard running only one of the two test files attributes the lines under only one identity. vitest.config.ts's own coverage-include comment documents this exact class from the 2026-07-24 codecov/patch (0%/99%) incident.

Fix. The exclusion behaviour is now exercised from both import paths rather than one:

  • screenshot-table-gate.test.ts (the src/ shim) gained the whenPaths exclusion cases, including that scope-matching and the committed-image check cannot disagree about what counts as scoped.
  • screenshot-table-gate-engine.test.ts (the engine source) gained direct matchesAnyWithExclusions cases, including the security one — an over-complex exclude glob excludes nothing, so it can only widen gate scope, never shrink it.

Not redundant duplication: each path is its own real file and its own coverage identity, so whichever shard picks up either suite, the lines stay attributed.

237 tests green across the three suites; typecheck clean; both engine files now report zero uncovered changed lines locally.

…-untested reject arms

Brings packages/loopover-engine/src/review/screenshot-table-gate.ts and
src/signals/change-guardrail.ts (and both re-export shims) to 100% statement,
branch and function coverage.

Three branches had no test on any identity:

- evaluateScreenshotTableGate's PRESENCE-mode freshness checkpoint. Matrix mode's
  identical correlation was pinned by #8866's tests, but presence mode runs it
  through a separate return site and never saw a headSha in a test. Presence mode
  is the DEFAULT shape, so the miss meant one table pasted on push #1 could hold
  the gate green for every later push -- the exact regression the checkpoint
  exists to stop. Six tests: first-push checkpoint, stale-on-new-head (asserting
  no checkpoint is re-issued, which would launder the staleness away after one
  extra push), re-affirmation with fresh URLs, custom message on the stale path,
  no-headSha degradation, and same-head replay.

- guardrailPathMatches' empty-path skip, a deliberate divergence from matchesAny's
  fail-safe: the boolean form matches an empty path under an over-complex glob,
  the structured form must not, because its output is rendered verbatim into
  public review text and audit metadata.

- extractTableRows' non-table reject arm, which is what stops ordinary PR prose
  (and a shell pipe inside backticks) from parsing as table rows.

Mirrored across both import identities so a sharded, flag-merged coverage upload
cannot report a branch as uncovered on one copy.
…s that transitively need it

validate-code failed on this PR with 'Cannot find module @loopover/contract/tools'
plus five downstream implicit-any errors in src/mcp/server.ts -- none of which this
PR touches.

#9530 added @loopover/contract, and src/mcp/server.ts imports @loopover/contract/tools.
Three turbo typecheck tasks pull that file into their program without any build edge
to the package:

- @loopover/ui#typecheck: apps/loopover-ui/tsconfig.json includes
  $TURBO_ROOT$/worker-configuration.d.ts, which imports './src/index' -- so the ENTIRE
  Worker is in the UI's typecheck program (src/index.ts -> src/api/routes.ts ->
  src/mcp/server.ts -> @loopover/contract/tools). Confirmed with tsc --explainFiles,
  not inferred. apps/loopover-ui has no package.json dependency on contract, so ^build
  never builds it.
- //#typecheck: a root task, where a root package.json dependency creates no build edge.
- @loopover/ui-miner#typecheck: reaches packages/loopover-miner/lib/**, which imports
  the package, and miner-ui has no dependency on it either.

Each already carries an explicit @loopover/engine#build edge for precisely this reason
-- turbo.json's own comment there describes the same scheduling race, observed
intermittently in validate-code, that bit here.

Cache-dependent, which is why it looked like flakiness: turbo caches these tasks, so
the failure only appears on a cache MISS. Other open PRs are green on cache hits.

Verified both directions from a clean contract build state (dist/ and .tsbuildinfo both
removed, --force):
- with the edges: 4 tasks successful, contract built first, typecheck passes
- without them: the identical six errors CI reported
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment