Skip to content

Ship feed-first public Tests with durable preview enrichment - #28

Merged
ItsJazii merged 2 commits into
mainfrom
codex/feed-first-phase-1
Aug 10, 2026
Merged

Ship feed-first public Tests with durable preview enrichment#28
ItsJazii merged 2 commits into
mainfrom
codex/feed-first-phase-1

Conversation

@ItsJazii

@ItsJazii ItsJazii commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Make / and /tests the public All Tests feed, with canonical Test detail pages and filters for model, harness, contributor, category, reasoning, and simple review status.
  • Replace the reusable benchmark/result submission flow with one authenticated Test form for prompt, declared model/version, harness, reasoning, settings, notes, and evidence.
  • Publish every safety-approved Test immediately as Awaiting review, without creating a judge run or 24-hour judge deadline.
  • Add declared-model browsing, a contributor dashboard, canonical redirects, the honest no-rankings-yet leaderboard state, and feed-first methodology/copy.
  • Run ZIP preview generation as independent durable queue enrichment, store derived artifacts separately, serve them from the cookieless user-content origin, and expose a private retry path without ever unpublishing an otherwise safe Test.
  • Keep historical benchmark/judge records readable and keep the legacy repair sweep from pulling new feed-only Tests into judging.

Product flow

  1. Anyone visits Benchmax and can browse every safe public Test, its prompt, declared setup, contributor, and uploaded output evidence.
  2. A contributor signs in and submits one Test with free-text model/version/harness/reasoning metadata and evidence.
  3. Mandatory quarantine, signature, ZIP, secret, and prompt-injection screening completes before publication.
  4. The Test becomes public immediately as Awaiting review.
  5. Compatible ZIP submissions receive best-effort automated preview/accessibility artifacts after publication; preview failure only shows Automated preview unavailable and is retryable by the owner.
  6. AI, admin, and trusted human reviews can later move Tests to Reviewed and Ranked.

The leaderboard is intentionally a later top-rated-submissions showcase across different prompts, not a scientific like-for-like benchmark. Same-prompt comparison groups remain later work.

Data and rollout notes

  • Migration 0023_overrated_leader.sql adds only durable preview-enrichment, generated-artifact, and preview-spend records. It is included for the eventual rollout but was not applied in this task.
  • No deployment, production resource, queue, route, or remote database change was performed.
  • benchmax.xyz/* still routes to the staging worker. That is intentional for now; the route moves when production rolls out.

Validation

Executed as one &&-gated chain through push:

npm run phase2:preflight && npx tsc --noEmit && npm run lint && npm test && git add -A && git commit && git push
  • 118 TypeScript unit/security/enrichment tests passed.
  • 4 evaluator and community lifecycle tests passed.
  • D1 migrations, foreign keys, append-only records, and frozen legacy contracts passed.
  • Production build passed.
  • 27 rendered route/security tests passed.

Open in Devin Review

Co-authored-by: Codex <codex@openai.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-authored-by: Codex <codex@openai.com>
@ItsJazii
ItsJazii merged commit 8d763f9 into main Aug 10, 2026
1 of 2 checks passed
@ItsJazii
ItsJazii deleted the codex/feed-first-phase-1 branch August 10, 2026 14:25

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 8 new potential issues.

Open in Devin Review

Comment on lines +136 to +140
file.originalSize <= MAX_TEXT_ENTRY_BYTES &&
selectedTextBytes + file.originalSize <= MAX_SCREENED_ARCHIVE_TEXT_BYTES
) {
selectedTextPaths.add(file.name);
selectedTextBytes += file.originalSize;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Credential checks silently stop inspecting files in large uploads

Text files inside an uploaded archive are skipped by the credential check (selectedTextBytes + file.originalSize <= MAX_SCREENED_ARCHIVE_TEXT_BYTES at lib/security/artifact-inspection.ts:137) once earlier files fill a 4 MB budget, so passwords or API keys in later files are published without being caught.
Impact: A contributor (accidentally or deliberately) can publish evidence containing live credentials by padding the upload with harmless text files first.

How the cumulative text budget changes scanning coverage

Before this change the ZIP filter selected every text entry up to MAX_TEXT_ENTRY_BYTES (1 MB each), and each selected entry was decoded and passed to detectSecretLabels (lib/security/artifact-inspection.ts:166-180). The new cumulative cap MAX_SCREENED_ARCHIVE_TEXT_BYTES = 4 MB (lib/security/artifact-inspection.ts:8) is applied in the same filter, so entries encountered after the budget is exhausted are never unzipped and therefore never secret-scanned, while inspectZipArchiveWithText still returns status: "approved".

Entry order is attacker-controlled (it is the ZIP central-directory order), so four 1 MB filler text files ahead of a .env are enough to bypass the check entirely. The cap appears to have been introduced to bound the new prompt-injection screening input; bounding only the textEntries returned for injection screening — while still decoding and secret-scanning every selected entry — would preserve the previous detection boundary.

Prompt for agents
In lib/security/artifact-inspection.ts the ZIP unzip filter now refuses to select text entries once a cumulative 4 MB budget (MAX_SCREENED_ARCHIVE_TEXT_BYTES) is exhausted. Because the same selected set is used both for the returned textEntries (new prompt-injection screening input) and for the existing per-file detectSecretLabels pass, the secret-detection coverage of large archives is silently reduced compared with the previous behaviour, and the archive is still reported as approved. Rework this so the cumulative bound only limits what is handed back for injection screening (or replaces it with a bounded truncation), while every text entry under MAX_TEXT_ENTRY_BYTES continues to be decoded and secret-scanned as before. Add a regression test covering an archive whose secret-bearing text file appears after several megabytes of benign text.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app/tests/page.tsx
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="max">Max</option>
<option value="unknown">Unknown / adaptive</option>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Filtering tests by "Unknown / adaptive" reasoning always returns nothing

The reasoning filter sends the value unknown (app/tests/page.tsx:106) while the submission form suggests contributors record the wording "Unknown / adaptive", so choosing that filter never matches any Test and the feed looks empty.
Impact: Visitors filtering the feed for adaptive-reasoning Tests always see "No Tests match these filters" even when such Tests exist.

Free-text reasoning versus fixed filter values

Reasoning is now free text: app/upload/UploadWizard.tsx renders an <input list="reasoning-options"> whose datalist offers None, Low, Medium, High, Max, and Unknown / adaptive, and the value is stored verbatim in showcases.reasoning_level (lib/data/showcases.ts createShowcaseDraft).

The feed filter compares with strict equality on the lowercased stored value: lower(showcases.reasoningLevel) = ${reasoning} in lib/data/showcases.ts:320-322. The option value unknown can therefore never equal unknown / adaptive. The same mismatch affects any other free-text reasoning wording, but the shipped suggestion list makes this one guaranteed to fail.

Suggested change
<option value="unknown">Unknown / adaptive</option>
<option value="unknown / adaptive">Unknown / adaptive</option>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 41 to 47
} catch {
judgeQueueDeferred = true;
enrichment = {
dispatchDeferred: true,
eligible: true,
enrichmentId: null,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Publication record can claim an automated preview was scheduled when none exists

When scheduling the automated preview fails outright, the publication record is written as if the Test were preview-eligible and merely delayed (enrichment = { dispatchDeferred: true, eligible: true, ... } at app/api/showcases/[id]/publish/route.ts:42-46), so the audit trail and the response claim a preview is coming for Tests that will never have one.
Impact: Operators reading publication records, and the contributor reading the publish response, are told an automated preview is pending for submissions that are not eligible for one.

Why the catch branch cannot know eligibility

scheduleShowcaseEnrichment (lib/data/showcase-enrichment.ts:38-69) already swallows queue-dispatch failures internally and returns { dispatchDeferred: true, eligible: true } in that case. The only way it throws is when ensureShowcaseEnrichment itself fails (for example a database error) — at which point no showcase_enrichments row exists and eligibility is unknown: the Test may have no compatible source ZIP at all.

Hard-coding eligible: true in the route's catch therefore reports an eligibility the code never determined. Reporting eligible: false (or a distinct unknown marker) would keep the record honest; the scheduled reconciliation sweep still creates and dispatches the row later if the submission really is eligible.

Suggested change
} catch {
judgeQueueDeferred = true;
enrichment = {
dispatchDeferred: true,
eligible: true,
enrichmentId: null,
};
}
} catch {
enrichment = {
dispatchDeferred: true,
eligible: false,
enrichmentId: null,
};
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +37 to +46
.where(
and(
eq(showcases.id, showcaseId),
eq(showcases.status, "draft"),
isNull(showcases.benchmarkVersionId),
inArray(showcases.safetyStatus, ["pending", "scanning", "approved"]),
),
)
.returning({ id: showcases.id });
return recorded ? { code, failedAt: now } : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Legacy drafts attached to a benchmark version cannot record or retry a processing failure

recordShowcaseProcessingFailure only matches rows with benchmarkVersionId IS NULL, and retryShowcaseProcessing applies the same predicate when loading the showcase. If an artifact scan throws for a pre-existing draft that still references a benchmark version, the upload-complete route returns 503 telling the contributor to "Retry processing from your dashboard", but no failure is recorded, the dashboard shows no retry affordance, and the retry endpoint would 404. Worth confirming that no legacy drafts remain (or widening the predicate) before rollout.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +223 to +228
if (finalResult.injectionFlag) {
await getDb()
.update(showcases)
.set({ rankingStatus: "moderation_hold", updatedAt: now })
.where(eq(showcases.id, artifact.showcaseId));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Prompt-injection heuristics now place submissions on permanent moderation hold

Artifact scanning now runs screenJudgeInjection over submitted text and bounded archive text, and any hit sets the showcase's rankingStatus to moderation_hold (lib/security/artifact-scanner.ts:223-228). This is a one-way transition with no automated clearing path in the diff, and the heuristic fires on ordinary phrases in submitted content (the corpus matches things like "ignore previous instructions" appearing anywhere in a page). Since Tests are now published without judging, the practical effect is that false positives silently and permanently disqualify a submission from ever being ranked. A moderator-visible signal plus a clearing path would be safer than an irreversible status write.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread lib/data/showcases.ts
Comment on lines 81 to +82
const id = crypto.randomUUID();
const slug = `${slugify(parsed.title)}-${id.slice(0, 8)}`;
const [benchmarkVersion, modelVersion, harness] = await Promise.all([
getDb()
.select({
id: benchmarkVersions.id,
category: benchmarkVersions.category,
canonicalPrompt: benchmarkVersions.canonicalPrompt,
})
.from(benchmarkVersions)
.innerJoin(
benchmarks,
eq(benchmarks.id, benchmarkVersions.benchmarkId),
)
.where(
and(
eq(benchmarkVersions.id, parsed.benchmarkVersionId),
sql`${benchmarkVersions.publishedAt} IS NOT NULL`,
),
)
.limit(1),
const slug = `${slugify(title)}-${id.slice(0, 8)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Auto-generated titles can produce a degenerate slug for non-Latin model names

With the title now optional, the fallback is ${modelLabel} model test and the public slug is slugify(title) plus eight hex characters. For a supplied title made entirely of non-ASCII characters, slugify can reduce to an empty string, yielding a slug like -1a2b3c4d. That value fails the slug pattern used by parseShowcaseSlug/parseReportTarget (lib/security/policy.ts:154) and by the user-content worker's RESULT_ARTIFACT_PATH, so the Test page would render while its evidence downloads and report link 404. This is pre-existing behaviour, but the free-text submission form makes non-Latin titles far more likely; consider falling back to the id when the slugified title is empty.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread lib/data/showcases.ts
Comment on lines 565 to 577
})
.from(showcases)
.innerJoin(users, eq(showcases.ownerId, users.id))
.innerJoin(
.leftJoin(
resultConfigurations,
eq(resultConfigurations.id, showcases.resultConfigurationId),
)
.innerJoin(
.leftJoin(
benchmarkVersions,
eq(benchmarkVersions.id, showcases.benchmarkVersionId),
)
.innerJoin(benchmarks, eq(benchmarks.id, benchmarkVersions.benchmarkId))
.leftJoin(benchmarks, eq(benchmarks.id, benchmarkVersions.benchmarkId))
.leftJoin(runs, eq(runs.showcaseId, showcases.id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Detail-page join relaxation leaves benchmark fields nullable for consumers

getPublicShowcaseBySlug switched resultConfigurations, benchmarkVersions, and benchmarks from inner to left joins so feed-only Tests resolve. The selected testSlug, testTitle, testVersion, and configurationHash values are now null for every new Test. The new /tests/[slug] page does not read them, but app/api/public/results/[slug]/route.ts returns this shape to external consumers, so that public API response silently changes from always-present test metadata to nullable fields. Worth confirming the documented API contract (and any consumer) tolerates the nulls.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +136 to +140
file.originalSize <= MAX_TEXT_ENTRY_BYTES &&
selectedTextBytes + file.originalSize <= MAX_SCREENED_ARCHIVE_TEXT_BYTES
) {
selectedTextPaths.add(file.name);
selectedTextBytes += file.originalSize;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Cumulative 4 MB text budget lets secret scanning skip files in large archives

inspectZipArchiveWithText now refuses to select any further text entry once earlier entries consume a cumulative 4 MB budget (lib/security/artifact-inspection.ts:136-140). The same selected set feeds the existing detectSecretLabels pass (lib/security/artifact-inspection.ts:166-180), so text files beyond the budget are never decoded and never checked for credentials, yet the archive is still returned with status: "approved" and can be published as public evidence. Entry order is attacker-controlled, so padding an archive with a few megabytes of benign text ahead of a .env/key file bypasses the mandatory secret check.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant