Skip to content

feat(review): gate surface entries on live grounding and functional-surface checks (#8908, #8909) - #9255

Merged
JSONbored merged 3 commits into
mainfrom
fix/8908-8909-surface-verification-gate
Jul 27, 2026
Merged

feat(review): gate surface entries on live grounding and functional-surface checks (#8908, #8909)#9255
JSONbored merged 3 commits into
mainfrom
fix/8908-8909-surface-verification-gate

Conversation

@JSONbored

@JSONbored JSONbored commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

computeGrounding and probeFunctionalSurface (src/review/content-lane/registry-logic.ts) have been fully implemented and unit-tested since the metagraphed port, but neither had a single caller outside its own test file. Confirmed still true on origin/main before starting: the only non-test references were the index.ts re-exports.

So the live surface-entry gate (assessSurfaceEntryrunSurfaceReviewrunRegistrySurfaceGate) merged a submission having never once looked at what its URLs actually serve — it validated that url/source_url were well-formed public HTTPS/WSS URLs and stopped there. Nothing confirmed the URL's content corroborated the claimed netuid/owner/host, and nothing confirmed an openapi/subnet-api/sse entry served the interface its kind declared.

This adds the missing half — the fetch plumbing and the gating decision — without modifying either primitive:

  • src/review/content-lane/surface-verification.ts (new): SSRF-guarded, redirect-following, truncation-tolerant probes of the entry's source_url and url; evidence extraction (title + tag-stripped text, with script/style contents dropped so a bundled script mentioning an unrelated "subnet 14" cannot forge a grounding signal); and the close-vs-hold policy.
  • orchestrator.ts: one optional injected verifyEntry hook on SurfaceReviewInput — same injected-I/O shape as the existing loadFile. Applied per appended entry, only to entries that already passed static validation. The orchestrator stays pure and domain-agnostic; any registry can supply its own verifier.
  • content-lane-wire.ts: builds the production verifier when the new flag is on.

Closes #8908
Closes #8909

Design decision 1 — block, hold, or warn?

Split, and deliberately asymmetric:

Outcome Disposition Why
Functional probe confirmed served:false CLOSE #8909 specifies "holding/closing on served: false". A 2xx whose body demonstrably is not the declared surface (an openapi url serving an HTML marketing page) is an objective, reproducible fact about the submission — the same class as the shape/kind violations assessSurfaceEntry already closes on (unsupported-shape), and trivially fixable by resubmitting with the right kind or url.
Grounding fails (strong below threshold) HOLD (manual) #8908 specifies no disposition, so this mirrors the codebase's own conventions for comparable heuristics. Grounding is a heuristic score over fetched page text, not a fact about the response: a legitimate surface genuinely fails it (a docs page that never prints its netuid, hosted on a domain unrelated to its source repo). source-evidence.ts already sets this precedent — a dead source only hard-closes when all authoritative sources failed and there is more than one; otherwise it routes to manual. Closing on a heuristic would one-shot-close good contributions. Crucially it no longer merges, which is the actual #8908 gap.
Either check inconclusive HOLD (manual) See decision 3.

Precedence is fail-closed-first: a confirmed functional failure outranks everything, then any inconclusive probe, then unconfirmed grounding.

The grounding threshold is SURFACE_GROUNDING_MIN_STRONG = 1 — one independent signal (netuid named, owner named, or host independently backed), net of computeGrounding's own cross-origin-redirect penalty. Not 2: metagraphed is a public registry whose own code comments state this is "ACCURACY corroboration, NOT ownership gating", and requiring two signals would hold a large share of legitimate submissions. One still forecloses the case the issue exists to catch — a surface whose evidence corroborates nothing about the subnet it claims.

Design decision 2 — flag gating and shipped default

New LOOPOVER_REVIEW_SURFACE_VERIFICATION, default "false", isSurfaceVerificationEnabled in the engine flag module alongside isContentLaneEnabled.

Deliberately its own flag rather than riding on LOOPOVER_REVIEW_CONTENT_LANE: this is the first thing in the lane that makes outbound requests to submitter-controlled URLs, and it can hold or close submissions that merge today. Both properties need to be rollable back without taking the (already-cutover) content lane down with them.

Per-repo scoping comes for free — the caller ANDs this with the lane's existing activation, so verification only ever runs where a RegistryLaneSpec already resolved. No new per-repo config surface, so no .loopover.yml schema / settings-resolver / migration parity work.

Flag-off (the shipped default), verifyEntry is undefined, the orchestrator runs no verification, no outbound probe is made, and the verdict is byte-identical. There is a regression test asserting exactly this with fetch stubbed to reject.

Design decision 3 — inconclusive must never look like a pass

Each check resolves to a strict three-state pass | fail | inconclusive, and inconclusive is never collapsed into either neighbour. It HOLDS, with its own reason code and its own public text, so "we could not check" never renders as "we checked and disliked it":

  • grounding-inconclusive — the source_url could not be fetched (throw, non-2xx, or no fetchable source declared). There is nothing to corroborate against.
  • grounding-unconfirmed — evidence was fetched and corroborates nothing.
  • functional-probe-inconclusive — the url could not be probed, returned non-2xx, returned a 2xx with an unreadable body (broken stream), or returned a 2xx with an empty body. That last one matters: probeFunctionalSurface would read a bare application/json content-type as a served API, so a blank 2xx is explicitly degraded rather than a pass.
  • verification-error — the verifier itself threw. makeSurfaceEntryVerifier is written not to throw; the orchestrator guards anyway so a future/third-party verifier cannot fail open.

fetchSurfaceProbe never throws — every failure mode collapses into an ok:false probe carrying an outcome token, so a thrown fetch can't escape into the review pipeline or be caught upstream and read as success.

Safety notes: isSafeHttpUrl is re-applied on every redirect hop (redirects are followed manually for exactly this reason — an https origin that 302s to 127.0.0.1 is refused), hops are capped at 4, bodies are capped at 64 KB, and a redirect landing on a different registrable domain sets cross_origin_redirect so computeGrounding applies its existing bait-and-switch penalty. A base-layer wss:// entry is not http-fetchable, so grounding rests on source evidence alone (still conclusive) and no functional kind is a wss kind.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (Closes #8908, Closes #8909).

Validation

  • git diff --check
  • npm run actionlint — not run; no workflow files touched.
  • npm run typecheck — clean (tsc --noEmit -p tsconfig.json --incremental false).
  • Coverage measured on the changed files: 100% stmts / 99.43% branch / 100% funcs / 100% lines, zero uncovered lines across surface-verification.ts, orchestrator.ts, and the engine flag.ts. Full unsharded test:coverage not run — see note below.
  • npm run test:workers — not run; no Workers-runtime surface touched.
  • npm run build:mcp / test:mcp-pack — not run; no MCP surface touched.
  • npm run ui:openapi:check — not run; no API routes or OpenAPI schemas touched.
  • npm run ui:lint / ui:typecheck / ui:build — not run; no UI files touched.
  • npm audit --audit-level=moderate — not run; no dependency changes.
  • New or changed behavior has unit tests for new branches, fallback paths, and boundaries.

Regenerated artifacts (committed): npm run cf-typegenworker-configuration.d.ts; src/env.d.ts declaration added to match the repo's optional-flag convention. npm run selfhost:env-reference and npm run db:migrations:check both re-run immediately before pushing — no diff, and no migration added (next free remains 0195).

If any required check was skipped, explain why:

  • The full local gate was intentionally not run for this maintainer PR; targeted suites plus a clean typecheck were used and CI confirms the rest. 447 tests pass across the eight content-lane suites (47 new in content-lane-surface-verification.test.ts, plus additions to the orchestrator, wire, and flag suites).
  • The full test/unit suite was run for regression safety: 22,477 tests, of which 17 fail in 12 files. All 17 were verified pre-existing on a clean origin/main by stashing this branch's changes and re-running the same 12 files — identical failures, identical count. None are content-lane related.
  • validate-tests is currently red on main itself, independently of this PR — the last four completed CI runs on main (3a3c1c29b, 32c7d5ad8, 5cece2759, 3e1654572) all fail that job, and 32c7d5ad8 is this branch's exact base. The failures here (config-templates, engine-parity-fixtures, predicted-gate-engine, worker-entry-boundary, linked-issue-label-propagation-fetch, reputation-wiring, selfhost-pg-retention, selfhost-metrics, salvageability, check-ui-kit-package, loopover-engine-scaffold, queue-5) are that same pre-existing set. Everything this PR is responsible for — validate-code, codecov/patch, codecov/project, and all 447 content-lane tests plus the engine's 746 node:test tests — is green.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized and low-noise. All gate summaries are fixed, engineer-authored strings — no fetched page content, URL, or contributor text is ever interpolated into public output.
  • Auth/CORS/session unaffected. The new outbound-fetch path reuses the existing isSafeHttpUrl SSRF guard and re-checks it per redirect hop, with negative-path tests for loopback, private IPs, non-https, malformed URLs, redirect-into-loopback, missing/unparseable Location, and redirect-loop exhaustion.
  • API/OpenAPI/MCP behavior unchanged.
  • No UI changes.
  • No docs/changelog changes needed.

Notes

Engine-package coverage: packages/loopover-engine/src/review/content-lane/flag.ts is measured by the engine's OWN Codecov flag, sourced from its node:test suite via c8 — the host vitest test does not count toward it. A matching packages/loopover-engine/test/content-lane-flag.test.ts is included so the new isSurfaceVerificationEnabled branch is covered on both sides. Engine suite: 746 tests pass.

Both issues are labelled maintainer-only and explicitly call for a maintainer decision on the fetch/gating design before implementation — this PR is that decision, made and documented above, plus the wiring. The Boundaries section of each issue asked that contributors not be unlocked without it; that stays as-is.

Follow-ups deliberately out of scope: the netuid-verification.ts taostats/public-registry identity signals (fetchSubnetRecord, deriveRegistryIdentityTokens, surfaceMatchesRegistryIdentity) are a separate corroboration axis that could feed the same hold decision, but wiring them needs the TAOSTATS_API_KEY secret story resolved and belongs in its own change.

@superagent-security

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (d04d0fe) and head (bf34c0e) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 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 bf34c0e Commit Preview URL

Branch Preview URL
Jul 27 2026, 10:46 AM

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 17 Tests Failed:

Tests completed Failed Passed Skipped
23085 17 23068 21
View the top 3 failed test(s) by shortest run time
test/unit/selfhost-pg-retention.test.ts > runRetentionPrune + processJob on the Postgres backend (#977) > processJob prune-retention deletes eligible rows and records a success audit event on Postgres
Stack Traces | 0.00462s run time
AssertionError: expected 3 to be +0 // Object.is equality

- Expected
+ Received

- 0
+ 3

 ❯ test/unit/selfhost-pg-retention.test.ts:119:44
test/unit/selfhost-pg-retention.test.ts > pruneExpiredRecords on the Postgres backend (#977) > deletes across multiple bounded batches and stops at the per-table cap, same as the SQLite path
Stack Traces | 0.0114s run time
AssertionError: expected +0 to be 4 // Object.is equality

- Expected
+ Received

- 4
+ 0

 ❯ test/unit/selfhost-pg-retention.test.ts:76:33
test/unit/loopover-engine-scaffold.test.ts > loopover-engine package scaffold > publishes only the build output and changelog, not source
Stack Traces | 0.0119s run time
AssertionError: expected [ 'dist/**/*.js', …(2) ] to deeply equal [ 'dist', 'CHANGELOG.md' ]

- Expected
+ Received

  [
-   "dist",
+   "dist/**/*.js",
+   "dist/**/*.d.ts",
    "CHANGELOG.md",
  ]

 ❯ test/unit/loopover-engine-scaffold.test.ts:30:29
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > never downgrades the locally-computed signal when the enabled bridge does not vouch
Stack Traces | 0.0184s run time
AssertionError: expected "vi.fn()" to be called once, but got 2 times
 ❯ test/unit/reputation-wiring.test.ts:441:25
test/unit/reputation-wiring.test.ts > getEffectiveSubmitterReputation (#4513, install-wide for a confirmed miner) > skips the miner-identity lookup entirely when the per-repo signal already justifies downgrading
Stack Traces | 0.0193s run time
AssertionError: expected 'neutral' to be 'low' // Object.is equality

Expected: "low"
Received: "neutral"

 ❯ test/unit/reputation-wiring.test.ts:520:26
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > keeps the pre-bridge output unchanged when any activation gate is off
Stack Traces | 0.0223s run time
AssertionError: expected "vi.fn()" to not be called at all, but actually been called 1 times

Received:

  1st vi.fn() call:

    Array [
      "https://api.gittensor.io/miners",
      Object {
        "headers": Object {
          "accept": "application/json",
          "user-agent": "loopover/0.1",
        },
        "signal": AbortSignal {
          Symbol(kEvents): Map {},
          Symbol(events.maxEventTargetListeners): 0,
          Symbol(events.maxEventTargetListenersWarned): false,
          Symbol(kHandlers): Map {},
          Symbol(kAborted): false,
          Symbol(kReason): undefined,
          Symbol(kComposite): false,
          Symbol(kTimeout): true,
        },
      },
    ]


Number of calls: 1

 ❯ test/unit/reputation-wiring.test.ts:419:31
test/unit/reputation-wiring.test.ts > getEffectiveSubmitterReputation (#4513, install-wide for a confirmed miner) > widens to the install-wide signal for a CONFIRMED miner when the per-repo signal alone stays neutral
Stack Traces | 0.0291s run time
AssertionError: expected 'neutral' to be 'low' // Object.is equality

Expected: "low"
Received: "neutral"

 ❯ test/unit/reputation-wiring.test.ts:482:26
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > REGRESSION (#6801): feature on upgrades a low local signal and changes the end-to-end gate decision
Stack Traces | 0.0457s run time
AssertionError: expected "vi.fn()" to be called 2 times, but got 3 times
 ❯ test/unit/reputation-wiring.test.ts:393:25
test/unit/config-templates.test.ts > config/examples review templates (#1682) > lints loopover.full.yml with zero warnings, including no retired top-level fields
Stack Traces | 0.156s run time
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "gate.mergeReadiness (\"off\") is set alongside an explicitly-authored mode for gate.linkedIssue, gate.duplicates, gate.slop.mode. The composite only fills in a sub-gate mode left unset -- it never overrides an explicitly-configured one, so those fields stay exactly as authored regardless of gate.mergeReadiness.",
+ ]

 ❯ test/unit/config-templates.test.ts:64:29
test/unit/selfhost-metrics.test.ts > DEFAULT_METRIC_META completeness (drift guard, 2026-07 fix) > every literal metric name emitted anywhere in src/ has a registered DEFAULT_METRIC_META entry
Stack Traces | 0.172s run time
AssertionError: expected [ …(2) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "loopover_orb_relay_multiple_live_enrollments_total",
+   "loopover_private_manifest_warnings_total",
+ ]

 ❯ test/unit/selfhost-metrics.test.ts:498:21
test/unit/queue-5.test.ts > queue processors > type label decoupling (#label-decoupling) > REGRESSION (#4528, PR #4494 shape): keeps the propagated labels on the PR's own merge-closed webhook, instead of falling back to the title guess
Stack Traces | 0.205s run time
AssertionError: expected [ 'gittensor:feature' ] to deeply equal [ 'gittensor:feature', …(1) ]

- Expected
+ Received

  [
    "gittensor:feature",
-   "gittensor:priority",
  ]

 ❯ test/unit/queue-5.test.ts:6660:34
test/unit/worker-entry-boundary.test.ts > worker entry boundary > does not reference pixelmatch, pngjs, visual-diff, gifenc, or sharp in worker-reachable source
Stack Traces | 0.507s run time
AssertionError: worker-reachable files must not mention Node-only visual diff/GIF/image deps: .../review/visual/visual-findings.ts: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   ".../review/visual/visual-findings.ts",
+ ]

 ❯ test/unit/worker-entry-boundary.test.ts:82:118
View the full list of 5 ❄️ flaky test(s)
test/contract/engine-parity.test.ts > predicted-gate engine parity (#2286) > 'merge-readiness-composite-preserves-e…' matches the committed golden output

Flake rate in main: 100.00% (Passed 0 times, Failed 7 times)

Stack Traces | 0.0151s run time
AssertionError: expected { predicted: true, …(11) } to deeply equal { predicted: true, …(10) }

- Expected
+ Received

  {
    "basis": "public_config",
-   "blockers": [
+   "blockers": [],
+   "conclusion": "success",
+   "confirmedContributor": undefined,
+   "funnel": null,
+   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
+   "pack": "gittensor",
+   "predicted": true,
+   "readinessScore": 80,
+   "summary": "No configured hard blocker was found. Advisory findings, if any, stay advisory.",
+   "title": "LoopOver Orb Review Agent passed",
+   "warnings": [
      {
        "action": "If this PR is intended to solve an issue, link it explicitly in the PR body.",
        "code": "missing_linked_issue",
        "detail": "No closing reference or linked issue number was found in the PR metadata/body.",
        "title": "No linked issue detected",
      },
    ],
-   "conclusion": "failure",
-   "funnel": null,
-   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
-   "pack": "gittensor",
-   "predicted": true,
-   "readinessScore": 80,
-   "summary": "No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.",
-   "title": "LoopOver Orb Review Agent: No linked issue detected",
-   "warnings": [],
  }

 ❯ test/contract/engine-parity.test.ts:46:21
test/unit/engine-parity-fixtures.test.ts > predicted-gate engine parity fixtures > keeps the 'merge-readiness-composite-preserves-e…' fixture parseable and aligned with its documented surface

Flake rate in main: 100.00% (Passed 0 times, Failed 7 times)

Stack Traces | 0.0118s run time
AssertionError: expected 'success' to be 'failure' // Object.is equality

Expected: "failure"
Received: "success"

 ❯ test/unit/engine-parity-fixtures.test.ts:29:31
test/unit/linked-issue-label-propagation-fetch.test.ts > fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate) > reward-label gate on direct author/assignee match (#9161) > propagates the reward label to its own issue's author when that author genuinely IS a repo maintainer (opt-in + maintainer check both satisfied)

Flake rate in main: 100.00% (Passed 0 times, Failed 7 times)

Stack Traces | 0.0281s run time
AssertionError: expected { labels: [ 'gittensor:bug' ], …(1) } to deeply equal { …(2) }

- Expected
+ Received

  {
    "inconclusive": false,
    "labels": [
      "gittensor:bug",
-     "gittensor:priority",
    ],
  }

 ❯ expectPropagation test/unit/linked-issue-label-propagation-fetch.test.ts:40:18
 ❯ test/unit/linked-issue-label-propagation-fetch.test.ts:1087:7
test/unit/predicted-gate-engine.test.ts > predicted-gate engine branch coverage (#2283) > exercises gate-advisory gateMode and blocker policy branches

Flake rate in main: 100.00% (Passed 0 times, Failed 7 times)

Stack Traces | 0.0238s run time
AssertionError: expected 'block' to be 'advisory' // Object.is equality

Expected: "advisory"
Received: "block"

 ❯ test/unit/predicted-gate-engine.test.ts:1558:55
test/unit/salvageability.test.ts > resolveAiReviewSalvageableHold > defaults: no configured floor uses the gate default, and a confidence-less blocker counts as certainty (at/above floor)

Flake rate in main: 100.00% (Passed 0 times, Failed 7 times)

Stack Traces | 0.0071s run time
AssertionError: expected undefined to be defined
 ❯ test/unit/salvageability.test.ts:88:18

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 2.02kB (0.03%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.45MB 2.02kB (0.03%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-PzIW0r5C.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-C3uhYXmy.js (New) 816.24kB 816.24kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-DuctO6_j.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-CyqdpLu2.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-Bh9nGbe4.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-DRylnBbF.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-BcykutFk.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/tuning-_XqW8VWg.js (New) 67.35kB 67.35kB 100.0% 🚀
assets/routes-BsZGHhYM.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-DlEncHgL.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-Ckczl-Dd.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/privacy-security-BQuEZgwY.js (New) 24.82kB 24.82kB 100.0% 🚀
assets/ui-vendor-CrG3EsFv.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-Dy3RTvW4.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-CDpM7oLk.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-CbXVS-et.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-B7W6RMoj.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-jJvvMZch.js (New) 15.37kB 15.37kB 100.0% 🚀
assets/playground-panel-CXrwhBx_.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-BbrNtY0g.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-BMMEu-LK.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-BIZTqcwq.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-CInEhjBO.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-Dj6Ybu_1.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DLoeoND1.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-BncFclSS.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-CbSw2tmF.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-CRP4dkB0.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-Df42OiWS.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-Cql3FPEe.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/docs.index-CZbgxw7C.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-BiV4nWIB.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BghiEgE_.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-Cc_RoL2v.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-dTjaWD42.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-DPioWVT6.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-Cet0ilgf.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CSc0EU9g.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-jlz4qqve.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BOTD6xCT.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-QhvzyJ6F.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-CUaryJBI.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-Cnpn4ZxM.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-PlGQVAxS.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BQo0PMr9.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-DnNRy9H9.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-QP3UiY8d.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-BWfd25dv.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-D0QbTMPT.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CHdL02pt.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-BiMfOfAL.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-gKSWPf7t.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-5spbupna.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-CViY1sHn.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-BqouVbaB.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BYdmYDyC.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-Brbpltd8.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-BLLJS9RV.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-By0O_3FK.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-Dmlnat3q.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-rWalh1nq.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DNDK07d2.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-CDTz6axV.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-KjzP9mWn.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-DkvKc8eG.js (Deleted) -816.24kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-DlrBeBYL.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-D1wOoueS.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-tNZvNmgl.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-Cl9RDoXi.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-BnuGFLFs.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/tuning-DGliDRPt.js (Deleted) -65.45kB 0 bytes -100.0% 🗑️
assets/routes-D-rG_PYd.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-D_okfM18.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-Duu_XZV3.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/privacy-security-BqzNQH41.js (Deleted) -24.7kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CWZJXKjt.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-B6Tojk4k.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-j0wpnRWU.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-DD6tzuuR.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-BjrkaR5l.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Csll7Hx8.js (Deleted) -15.37kB 0 bytes -100.0% 🗑️
assets/playground-panel-BMELUXBf.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-z8pwc337.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-DIr7eX2g.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-CrY2Jiz2.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DkVab-Im.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-HaD8xYe-.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-BkHB8Wxe.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-DQPQeMDW.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-SKt4ouIK.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-DDwTA_aC.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-Cjbf5Ki4.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-C_250m-R.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/docs.index-BFfns2kg.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-BAgAJlYd.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-B3jsL9aH.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api--eOp3xs2.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-zwz80XSR.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-oq5PAVwo.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-1ufs7b4B.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BDglh5ye.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CEfqZSeh.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-CJX2tbcM.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-BjXSzDaH.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-Ohvnl2zJ.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-BG9u2KTZ.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-BwTxtDCL.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-Ce4tIE18.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-C_WgSe_W.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CXAOfJdr.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-B1KLwrux.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-Caa-2pOI.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-CC20bArn.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CjcRm-wu.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-CXFkW5OH.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-B-scgHdk.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-D2rWnG7Q.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-D_q_DENP.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-Cv2wpPDA.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-CzM9Fm6-.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BhEHtcUt.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-BFS0cagl.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-wGbUwhhR.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-DSrsC74z.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CFrcX_Kf.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-BO5jtACK.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-27 10:40:34 UTC

15 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
This wires the previously-orphaned computeGrounding/probeFunctionalSurface primitives into a live, SSRF-guarded fetch layer and an injected verifyEntry hook on the orchestrator, gated behind its own default-off flag so the existing lane stays byte-identical until enabled. The tri-state pass/fail/inconclusive design with fail-closed-first precedence (functional fail > functional inconclusive > grounding inconclusive > grounding fail) is coherent and matches the stated #8908/#8909 intent, and the verifier-throws-never-merges guard in verifyMergedEntries is correctly defensive. The SSRF guard is re-applied on every redirect hop (fetchSurfaceProbe's loop calls isSafeHttpUrl(currentUrl) each iteration), which is the detail most likely to be gotten wrong and appears correct here.

Nits — 6 non-blocking
  • src/review/content-lane/surface-verification.ts: the external brief flags depth-5 nesting in fetchSurfaceProbe's redirect loop (around line 143) — a small early-return extraction for the redirect branch would flatten this without changing behavior.
  • src/env.d.ts is now ~531 lines per the size-smell scan; not this PR's fault alone (it's an append-only env doc file) but worth a future split if it keeps growing.
  • MAX_PROBE_REDIRECTS=4 and PROBE_TIMEOUT_MS=10_000 are reasonable but undocumented against any real-world worst case (slow doc host chains) — a one-line comment on why 4/10s was picked would help future tuners.
  • surface-verification.ts:307 (FUNCTIONAL_INCONCLUSIVE_REASON etc.) — the four reason-code exports are correctly excluded from registry-logic's REVIEWER_CLOSE_REASONS per the comment, but nothing enforces that exclusion stays true if that set is refactored; consider a cheap unit assertion that these four codes are disjoint from REVIEWER_CLOSE_REASONS.
  • Confirm PROBE_FETCH_HEADERS' Accept-Language/User-Agent choices don't need periodic updates as browser UA strings age out (source-evidence.ts sets a precedent worth cross-checking stays in sync).
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

CI checks failing

  • validate
  • validate-tests

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #8908, #8909
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 331 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 331 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR adds a new surface-verification.ts module that fetches source_url/url content, feeds it into computeGrounding (and probeFunctionalSurface), and wires the result through a verifyEntry hook into runSurfaceReview/orchestrator.ts and content-lane-wire.ts's runRegistrySurfaceGate, gated behind an explicit new flag (LOOPOVER_REVIEW_SURFACE_VERIFICATION, default off) as the issue's boundaries requ

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 13 PR(s), 331 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: success
  • config: 03a7f8b529a9 · pack: oss-anti-slop
  • record: ecf0cf3e418d (schema v3, head 770f27b)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

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


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 27, 2026
…urface checks (#8908, #8909)

`computeGrounding` and `probeFunctionalSurface` have been fully implemented and
unit-tested in registry-logic.ts since the metagraphed port, but neither had a
single caller outside its own test file. The live surface-entry gate
(assessSurfaceEntry -> runSurfaceReview) merged a submission having never once
looked at what its URLs actually serve: it validated that `url`/`source_url`
were well-formed public HTTPS/WSS URLs and stopped there.

Add the missing half — the fetch plumbing and the gating decision — without
touching either primitive:

- surface-verification.ts: SSRF-guarded, redirect-following, truncation-tolerant
  probes of the entry's `source_url` and `url`; evidence extraction (title +
  tag-stripped text, script/style dropped so bundled code cannot forge a signal);
  and the close-vs-hold policy.
- orchestrator: one optional injected `verifyEntry` hook, applied per appended
  entry, only to entries that already passed static validation. The orchestrator
  stays pure and domain-agnostic — any registry can supply its own verifier.
- content-lane-wire: builds the verifier when the new flag is on.

Policy. A CONFIRMED functional failure (a 2xx whose body demonstrably is not the
declared surface — an `openapi` url serving HTML) CLOSES: an objective,
reproducible, trivially-fixable fact, in the same class as the shape violations
the entry validator already closes on. Unconfirmed grounding HOLDS rather than
closes — it is a heuristic over page text, and a legitimate surface can fail it,
so closing on it would one-shot-close good contributions. It no longer merges,
which is the actual #8908 gap.

Three-state, never fail-open. Each check resolves to pass / fail / inconclusive
and these stay strictly distinct: an unreachable probe, a 2xx with an unreadable
or empty body, a missing source, or a throwing verifier all HOLD with their own
reason code, never a pass. A check that silently passes when it could not run
launders "we didn't look" into "we verified".

Flag-gated and OFF by default, on its own flag rather than the content lane's:
this is the lane's first outbound request to submitter-controlled URLs and it can
hold or close submissions that merge today, so both need independent rollback.
Flag-off, no probe is made and the verdict is byte-identical.
… node:test suite

The host vitest test for isSurfaceVerificationEnabled/isContentLaneEnabled
(test/unit/content-lane-flag.test.ts) doesn't count toward the engine
package's own Codecov flag -- that's measured from packages/loopover-engine's
node:test suite via c8 (#9064), which had no equivalent test for this file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

1 participant