Compliance/l1 l15 - #44
Conversation
- events + publicEventPreviews schema: aiGenerated/humanReviewed/modelUsed/promptVersion - applyEventSummaryResult stamps disclosure fields on every summary write (publication impossible with aiGenerated unset); summarizationNode records primary vs fallback model - backfill migrations for events + previews (modelUsed='unrecorded' for legacy rows) - SSR AiDisclosureLabel adjacent to summaries on event pages (all perspective tabs) - server-rendered JSON-LD with creativeWorkStatus + IPTC digitalSourceType trainedAlgorithmicMedia; aiGenerated in all public feed payloads - methodology page: 'Generăm rezumatele cu AI' section (#rezumate-ai anchor) - tests: publish-gate disclosure invariants, label rendering ro/en Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/compliance.ts: MAX_SNIPPET_CHARS=120 + sentence/word-boundary truncation - write-time enforcement: ingestion.insertArticles (rssSnippet), enrichment.markArticleEnriched (extracted summary), clustering heuristic representative snippet capped at 120 - migrations.backfillSnippetCeiling truncates existing stored rows - web: shared <Snippet> render guard (hard 120-char ceiling) used by articles-list + source page, both with adjacent canonical links - docs/compliance-third-party-text-audit.md: display vs processing field audit - tests: 500-char RSS description truncated at storage AND render; web/backend ceiling cross-check Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/verbatimOverlap.ts: ≥8-word contiguous n-gram detection (configurable event_summary_max_verbatim_ngram), normalized punctuation/casing, exemptions for quoted text and entity/number-dominated spans - summarizationNode: post-generation check over titles/snippets/summaries/ atomic facts/transient bodies; up to 2 paraphrase-instruction regenerations; persistent failure → markSummaryJobBlockedVerbatim (terminal, spans recorded) - applyEventSummaryResult refuses failing checks; records pass on event (lastOverlapCheckAt/Passed) and job (overlapCheckJson) - prompt v6: absolute paraphrase mandate (no ≥8-word copies, no copied sentence structure, attributed ≤10-word quotes only) - tests: lifted 12-word sentence blocked; paraphrase passes; quote/entity exemptions; publish-gate invariants Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…queue - lib/grounding.ts: sentence splitter, cosine sim, accusation lexicon (config- extendable via accusation_lexicon), person/org entity heuristic, risk flags - summarizationNode.verifySummaryGrounding: embedding-similarity first pass (candidate shortlist) + decisive one-call LLM entailment per sentence; unsupported sentences stripped, too-many-failures or empty neutral → blocked_ungrounded (terminal); per-sentence support stored in new summaryGrounding table with supporting article IDs - NER risk gate: named entity + accusation term → summaryReviewQueue hold, never auto-published; pending-review events excluded from re-enqueue; admin approve (optionally edited → humanReviewed=true) / reject via decideSummaryReviewForAdmin; minimal /admin/review UI with flagged-sentence highlighting - applyEventSummaryResult refuses failing grounding; getSummaryGrounding public query powers per-sentence source attribution tooltips on event pages - tests: risky summary held + never published; pending review blocks re-enqueue; failing grounding unappliable; attribution query resolves names Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/tdmPolicy.ts: pure parsers for TDM-Reservation header, tdmrep.json, tdm-reservation meta, RFC 9309 robots.txt (BiviantBot/*/AI-convention tokens incl. GPTBot/CCBot/anthropic-ai/Google-Extended), noai meta + X-Robots-Tag, ai.txt; evaluateTdmSignals → full|rss_only (any opt-out → rss_only; unreachable robots.txt fails closed) - domainPermissions table (24h TTL) + upsert with restrictiveness ordering; more-restrictive transitions auto-purge extracted content (summaries, atomic facts; blocked → articles discarded incl. snippets); manual blocks (publisher opt-out) never loosened by the resolver; blockDomainForAdmin - domainPermissionsNode: resolver action fetching all six signal sources with BiviantBot UA; ensureDomainPermissions batch helper failing closed to rss_only on resolution errors - gates: enrichment extraction requires state=full (else rssOnlyArticleContent, RSS metadata only); summarizer refuses full-text input from non-full domains and logs permission state per source per run - tests: header/robots/meta/ai.txt parsing; restriction purge; manual-block stickiness; relaxation does not purge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/botIdentity.ts: single honest UA 'BiviantBot/1.0 (+https://biviant.com/bot; …)' + From header on ALL outbound fetches (extraction, RSS, images, share assets); removed browser-masquerade header profiles and referer spoofing - lib/politeFetch.ts: per-domain limiter (≥1.5s spacing + jitter, Crawl-delay as minimum, max 2 concurrent) + exponential backoff on 429/5xx honoring Retry-After; wired into article extraction and RSS ingestion - conditional feed requests: If-None-Match/If-Modified-Since from ingestionMeta (new lastEtag/lastModifiedHttp), 304 handled as success-no-change - /bot page (identity + every opt-out channel); /publishers opt-out/takedown form → publisherRequests table (received→decided→executed timestamps) + pipeline alert; admin approval blocks the domain and purges content in one action (via L5 domainPermissions); rate-limited public submission - schema also carries the L7 generationAudit table + module scaffolding (pipeline wiring lands in the next commit) - tests: UA/From on outbound requests, per-domain spacing/concurrency, Retry-After backoff, submission lifecycle + rate limit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- appendGenerationAudit is the sole writer: versioned per event, each record links its predecessor (supersedesAuditId); no update/delete mutation exists - wired into every pipeline stage in the same transaction as the state change: published (summary text, model, prompt version, source article IDs + content hashes + fetch timestamps + L5 permission state at fetch, overlap + grounding results, disclosure label version, publishedAt), blocked_verbatim, blocked_ungrounded, held_for_review, review_approved/rejected - getGenerationAuditForAdmin reconstructs the full chain in one query - tests: publication record completeness, linked versions, module exposes no mutator, grounding-block audit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion) - on-page report form (#raporteaza) on every event page — category (factual error/defamation/copyright/illegal), claim, optional reporter contact; AI-disclosure label now links straight to it; rate-limited public mutation - contentReports table + /admin/reports queue: dismiss / correct / unpublish, each requiring a statement of reasons (stored + emailed to the reporter via Resend when contact provided); defamation/illegal reports raise an error-severity operator alert immediately - one-click unpublish: events.unpublishedAt flag → getEventBySlug null, preview deleted, trending snapshot rebuilt, pipeline eligibility blocked, L7 audit 'unpublished' appended; unpublishEventForAdmin standalone - correct: clears freshness markers, enqueues correction_requested job, appends L7 'corrected' audit version - /contact: DSA single point of contact for users and authorities (ro/en) - tests: report round-trip, unpublish removes all public surfaces + blocks re-summarization, correction queues regeneration, statement-of-reasons required, rate limits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- three tiers enforced: favicons/logos allowed (cached small); publisher og:image HOTLINK-ONLY, small + attributed + linked to the original article, gated on L5 permission state; editorial images never fetched/stored - share cards no longer download the publisher photo into a stored JPEG — brand layout + source logos only - global kill switch (og_image_display_enabled config) + per-domain (domainPermissions.imagesDisabled + setDomainImagePolicyForAdmin), both enforced in getEventBySlug and preview sync - L5 opt-out purge now clears article thumbnails and event heroes hotlinked from the domain (purgeDomainEventImages, previews resynced) - event page hero: <figure> capped width, Foto attribution caption, wrapped in a link to the source article - lint test: ctx.storage.store allowed only in shareAssetsNode.ts; renderer provably no longer fetches data.imageUrl; kill-switch + purge tests - docs/compliance-image-policy.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- exportMyData (authenticated): full JSON export — account, profile, stats,
private context, interactions (bookmarks + reading history), insights,
quiz attempts, push tokens, guest merges, briefing sends, waitlist entry
(+ future consent records); profile page 'Download my data' button
- deleteMyAccount (authenticated, self-service): cascade via extended
authMaintenance.deleteAppUserData now covering EVERY userId table
(userStats, userPrivateContext, userInsights, interactions, guestMerges,
pushTokens, briefingSends, quizAttempts) + waitlist row by email; Better
Auth sessions/accounts/verifications/user removed; PostHog person+events
erasure scheduled (previously orphaned helper now wired)
- profile page: two-step confirm delete replacing the 'request via contact'
link
- tests: schema cross-check (any table referencing v.id('users') must be in
the cascade), full-cascade erasure to zero rows, Google OAuth config adds
no extra scopes (defaults = openid/email/profile only)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- retention.RETENTION_POLICY: single source of truth per data class (Art. 30 input): waitlist unengaged 90d, reading history 18mo, unverified accounts 7d (existing cron), user insights 30d, transient article body text 0 (never persisted by construction) - daily crons: purge unengaged waitlist signups, 18mo+ interaction history, expired personalized insights — batched with self-continuation - every purge run logged to pipelineRunLogs (data class, deleted count, timestamp) - tests: time-shifted fixtures (stale purged, engaged/recent kept), run logging, policy-as-source-of-truth, cron registration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/consent.ts: versioned WAITLIST_CONSENT_TEXT + hash; the form renders it
verbatim ('Trimite-mi un e-mail când…') with the privacy policy link
adjacent to submit and the opt-out channel named at collection — no
pre-checked boxes, no ToS bundling
- every signup stores consentAt, consentIp (via TanStack server fn),
consentTextVersion+hash, consentSourcePage, and a unique unsubscribeToken;
re-subscribing records fresh consent
- one-click unsubscribe: /unsubscribe?token=… (no login, no address in URL,
immediate effect); List-Unsubscribe + body links now token-based
- suppression list checked by ALL send paths: welcome/invite actions refuse
unsubscribed/bounced via getSendableWaitlistEntry; legacy rows get tokens
lazily
- tests: consent record persisted per signup, token round-trip + immediate
suppression, invalid token opaque failure, fresh-consent resubscribe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PostHog (EU-hosted) switched to cookieless: persistence='memory' — no ph_* cookie/localStorage device id before or after consent, session recording + surveys disabled; anonymous visitors get per-visit ids only - docs/compliance-storage-inventory.md: complete cookie/localStorage inventory — fresh load sets only the TanStack scroll-restoration sessionStorage entry; auth cookies/locale/theme/search history only after explicit user action; no third-party trackers - e2e/consent-free-storage.spec.ts (verified passing against the dev server): fresh load with no interaction sets zero non-essential cookies/storage; no ph_* persistence across navigation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- restored the commented-out business-identity sections (contact, despre,
termeni, politica-confidentialitate) — fixes the two failing BIV-803
footer-pages tests — and added Reg. Com. + cod fiscal placeholders
({{TODO}} pattern, cross-checked with FOOTER_TODO.md; nothing invented)
- sitewide footer: Legea 365/2002 identity line (all TODO placeholders) +
links to /publishers and /bot
- /termeni: new 'Semnalarea conținutului și corecții (DSA)' section
describing the report mechanism, decisions with statement of reasons, and
content policies
- /politica-confidentialitate: retention section now renders from the same
RETENTION_POLICY object the purge crons enforce (moved to
lib/retentionPolicy, exported to the web app); cookies section rewritten to
the L13 cookieless reality (banner-free, storage inventory referenced)
- no EU ODR link anywhere (platform shut down July 2025); L15 adds the
codebase guard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .github/workflows/ci.yml (first CI in the repo): typechecks + full backend and web vitest suites on every push/PR; mark as required check on main so Vercel promotion is blocked on failure - backend compliance.test.ts invariant bundle: snippet ceiling exactly 120 (raising it fails the build), verbatim n-gram default 8, BiviantBot UA shape, retention policy values, pipeline wiring (permission gate + overlap + grounding + NER before publish, audit on publish), no EU ODR link anywhere in the repo, storage.store allowlist, PostHog memory persistence, suppression gate on both send paths - e2e/compliance-event-page.spec.ts (verified passing): curl-equivalent raw HTML of a real event page contains the visible AI label AND the JSON-LD trainedAlgorithmicMedia/creativeWorkStatus markers; discovery via sitemap - fixed pre-existing sitemap rebuild bug (two paginated queries per mutation always threw → sitemap stuck on fallback); uses .take() now - updated stale event-card design assertions to the current responsive layout — web suite fully green (71/71), backend 267 passing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop Agerpres as an ingested source: feed definition, reputation row, headline-suffix stripping, and the two Agerpres-only image-cleanup migrations (clearHtmlPageImageUrls / clearHtmlPageImageForEventSlug, whose only pattern was foto.agerpres.ro). Feed mix 8:5:6 -> 8:4:6. De-brand the neutral bias-axis anchor in the prompt and spec. The generic HTML-in-image-slot guard in lib/imageVerification.ts stays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the {{TODO: entitate juridică}} / Reg. Com. / cod fiscal /
adresă sediu placeholders from the footer, contact, despre, termeni,
and privacy pages (no company entity to disclose). Keep the contact
e-mail and effective-date TODOs. Drop the now-moot no-fabricated-identity
test and the four identity rows from FOOTER_TODO.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Widen the event hero figure from max-w-xl (~65% of the column) to w-full so it matches the title. Attribution, source link, aspect ratio, and lazy-load are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WalkthroughThis PR adds CI validation, compliance controls for snippets, storage, consent, retention, crawler access, images, AI summaries, reports, publisher takedowns, GDPR actions, and unsubscribe flows. It also adds related backend schemas, web routes, admin interfaces, disclosures, documentation, migrations, and automated tests. ChangesWeb compliance surfaces
Backend policy and data workflows
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 35
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/backend/convex/waitlist.ts (1)
28-46: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftDo not treat a client-supplied IP address as consent provenance.
clientIpcomes directly from the public mutation payload and is persisted without validation, so any caller can forge the audit record. Capture it through a trusted server boundary, or explicitly store it as client-reported metadata; also validateconsentSourcePageas an allowed application path.As per path instructions, “Focus on Convex best practices” and “Review database schema changes carefully.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/backend/convex/waitlist.ts` around lines 28 - 46, Update the waitlist mutation handler around consentRecord so clientIp is not persisted as trusted consent provenance: obtain the IP from a trusted server-side boundary, or rename and store it explicitly as client-reported metadata. Validate consentSourcePage against the application’s allowed paths before including it in the record, while preserving the existing consent version, hash, and timestamp fields.Source: Path instructions
packages/backend/convex/summarization.ts (1)
890-955: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRequire overlap and grounding on publish These fields are optional, but omitting either skips the gate entirely and still allows a summary to publish. Make the publish path fail closed when either check is missing, or move legacy compatibility to a separate path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/backend/convex/summarization.ts` around lines 890 - 955, The publish handler around the summary application logic must fail closed when overlapCheck or grounding is absent, not only when a provided check fails. Require both checks before allowing publication, returning the appropriate rejection result for missing or failing validation; preserve any legacy compatibility through a separate path rather than optional gates in this handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 22-26: Replace the floating major-version references in the
workflow steps using actions/checkout, pnpm/action-setup, and actions/setup-node
with their verified full commit SHA pins, retaining the existing action versions
and configuration. Ensure Dependabot recognizes and verifies the SHA-pinned
dependencies.
- Line 22: Update the actions/checkout@v4 step in the CI workflow to set
persist-credentials to false, ensuring subsequent pnpm install and vitest steps
cannot access persisted GITHUB_TOKEN credentials.
In `@apps/web/src/components/feed/report-error-form.tsx`:
- Around line 59-60: Update the success message in the report form’s sent-state
rendering and the error message near the corresponding error-state rendering to
include the required ARIA live-region attributes, including aria-live and role.
Preserve their existing text, styling, translation keys, and conditional
rendering.
In `@apps/web/src/components/profile/AuthenticatedProfile.tsx`:
- Around line 97-118: Separate the error handling in handleDeleteAccount so the
catch for deleteMyAccount only reports deletion failures and resets deletion
state. After a successful deletion, handle authClient.signOut independently
using the existing handleSignOut pattern, while preserving the finally redirect
to "/" and avoiding any "profile.deleteFailed" toast when sign-out alone fails.
In `@apps/web/src/lib/getClientIp.ts`:
- Around line 9-13: Stop using the browser-provided result of getClientIp for
persisted consent provenance, since x-forwarded-for and x-real-ip can be forged
by the client. Update the mutation or consent-recording flow that consumes
clientIp to obtain the address from trusted server-side request context, or omit
persistence and treat the value as advisory only; do not rely on
apps/web/src/lib/getClientIp.ts for authoritative data.
In `@apps/web/src/lib/snippet.ts`:
- Around line 18-22: Update the snippet truncation logic around the budget slice
in the snippet-generation function to avoid ending on a dangling high surrogate:
if the cutoff splits a surrogate pair, back up one UTF-16 unit before
determining the word boundary and appending ELLIPSIS. Add a regression test
covering Unicode input such as an emoji at the cutoff, while preserving existing
word-boundary and ellipsis behavior.
In `@apps/web/src/routes/admin.reports.tsx`:
- Around line 107-121: Update the reasons textarea in the report decision form
to include the native required constraint and a minimum length of 5 characters,
matching the existing handleDecision validation while preserving the current
value and onChange behavior.
- Around line 13-15: Add an admin authentication guard to the shared parent
route for the /admin/* tree, or to AdminReportsRoute via beforeLoad if no parent
exists, so unauthenticated users are rejected or redirected before the route
component and Convex query execute. Reuse the project’s existing auth-check and
redirect symbols rather than introducing a separate mechanism.
In `@apps/web/src/routes/admin.review.tsx`:
- Around line 142-148: Update the flagged-field rendering in the reduce call so
every sentence in flagged is processed, rather than stopping once the
accumulator becomes a ReactNode. Refactor or reuse highlightFlagged to apply all
flagged sentences in a single pass while preserving the existing highlighted
output for each match.
In `@apps/web/src/routes/event`.$slug.tsx:
- Around line 296-301: Update the JSON-LD serialization in the event route’s
script element to escape HTML-breaking characters, especially `<` (and
optionally `>` and `&`), in the JSON.stringify(jsonLd) result before assigning
it to dangerouslySetInnerHTML. Preserve the existing jsonLd structure and
application/ld+json behavior while ensuring embedded values cannot prematurely
close the script tag.
- Around line 360-392: Guard the image figure rendering in the event image block
behind a truthy imageArticle check. Keep deriving imageArticle from the matching
article or articles[0], but render the <figure>, link, image, and caption only
when an article exists so the anchor never receives an undefined canonicalUrl.
In `@apps/web/src/routes/publishers.tsx`:
- Around line 91-96: Update apps/web/src/routes/publishers.tsx lines 91-96 by
adding role="status" and aria-live="polite" to the success message paragraph.
Update lines 103-152 by adding aria-label values matching the visible labels to
the domain Input, contact Input, request-type select, and message textarea;
preserve the existing label associations.
In `@packages/backend/convex/authMaintenance.ts`:
- Around line 34-69: Update the user-deletion mutation around deleteByUserIndex
and the quizAttempts cleanup to process records in bounded batches rather than
collecting and deleting entire tables in one transaction. Paginate each table’s
by_user/by_user_quiz query, delete one batch, and continue until no records
remain; only delete the user record after every table, including quizAttempts,
has been fully processed.
In `@packages/backend/convex/dataRights.ts`:
- Around line 50-74: The GDPR export flow containing the interactions,
userInsights, quizAttempts, and briefingSends queries must return every matching
row instead of silently applying EXPORT_ROW_CAP. Replace the capped
single-response approach with a paginated, multipart, or asynchronous archive
mechanism that fully exports these datasets and provides continuation or
completion handling.
In `@packages/backend/convex/domainPermissions.ts`:
- Around line 72-99: Normalize args.domain once at the start of each
domain-permission operation, including the paths at the referenced ranges, using
the established canonical domain utility. Use the normalized value consistently
for by_domain queries, stored row.domain values, logs, and scheduled purge
operations so equivalent inputs share one record. Preserve the schema shape and
existing permission state behavior while applying canonicalization to every read
and write.
- Around line 184-199: Update the purge condition in the domain-permission flow
to also trigger when article.entities contains extracted data, and extend the
corresponding ctx.db.patch for that article to clear entities. Preserve the
existing cleanup behavior for summary, atomicFacts, image fields, and
extractionQuality.
In `@packages/backend/convex/domainPermissionsNode.ts`:
- Around line 25-38: Update fetchWithTimeout and its redirect handling to
prevent SSRF: resolve each target hostname, reject loopback, private,
link-local, reserved, and otherwise non-public IPs, disable automatic redirects,
and manually follow each Location response only after applying the same
validation to the next URL. Preserve the existing timeout and request headers
while enforcing validation on the initial URL and every redirect hop.
- Around line 162-176: Update the domain-resolution loop over toResolve to run
resolveDomainPermission calls with a small bounded concurrency limit instead of
awaiting each domain sequentially. Preserve per-domain result.set behavior,
including the existing rss_only fallback and warning when a resolution fails.
Ensure all scheduled resolutions complete before returning the aggregate result.
In `@packages/backend/convex/enrichmentNode.ts`:
- Around line 807-842: Update the enrichment flow around ensureDomainPermissions
and extractArticleContentForEmbedding so authorization is performed for the
final host actually fetched, not only article.canonicalUrl. Resolve each
article’s destination URL, obtain permission for every resulting host including
redirect destinations, and allow full extraction only when the fetched host is
permitted; otherwise use rssOnlyArticleContent. Preserve the existing RSS-only
behavior and ensure no response body is read or processed before destination
authorization succeeds.
In `@packages/backend/convex/ingestion.ts`:
- Around line 1342-1384: Remove the outer botFetchHeaders wrapper from the
politeFetch call in the ingestion flow, passing the Accept, Accept-Language, and
conditional request headers directly through the headers option. Keep
politeFetch responsible for applying the standard bot headers and preserve the
existing conditional-header behavior.
In `@packages/backend/convex/lib/imagePolicy.ts`:
- Around line 46-64: Update the image policy function containing the hostname
lookup to accept the article/source publisher domain and evaluate its domain
permission first, returning the same denied result for disabled or non-full
publisher states. Only if the publisher has no blocking permission should the
existing image-host lookup run, preserving current CDN-host behavior for
otherwise allowed publishers.
- Around line 38-43: Update the killSwitch parsing logic in the image-policy
function so malformed JSON returns false immediately instead of continuing to
per-domain checks. In the catch block, emit an operational error alert using the
existing logging or alerting mechanism, while preserving the current false
result when the parsed value is explicitly false.
In `@packages/backend/convex/lib/publicEventPreviews.ts`:
- Around line 249-252: Remove the true fallback that treats missing provenance
as AI-generated: in packages/backend/convex/events.ts lines 469-475, expose
explicit event provenance; in packages/backend/convex/lib/publicEventPreviews.ts
lines 249-252, derive or backfill the accurate provenance when persisting
previews; and in packages/backend/convex/lib/feedSerialization.ts lines 116-118,
serialize the persisted value without inventing AI provenance.
In `@packages/backend/convex/lib/tdmPolicy.ts`:
- Around line 161-173: The declaresNoAi parser currently requires the robots
meta tag’s name attribute to precede content; parse the attributes independently
so either order is accepted while preserving case-insensitive noai detection.
Add a reversed-attribute regression case in
packages/backend/convex/tdmPermissions.test.ts at lines 101-109 covering a meta
tag with content before name.
- Around line 88-112: Update robotsGroupFor() to merge all matching exact-agent
or wildcard groups into one RobotsGroup before robotsDisallowsAll() evaluates
disallow and allow rules, so later Allow entries are preserved; retain
exact-agent precedence over wildcard groups. Add regressions in
packages/backend/convex/tdmPermissions.test.ts covering duplicate-agent groups
and Disallow: / combined with a path-specific Allow.
In `@packages/backend/convex/publisherRequests.ts`:
- Around line 61-67: Update the pipelineAlerts insert in the publisher request
handler to remove contact from details; store only requestId, domain, and
requestType, preserving the existing alert severity, code, message, and
timestamp.
- Around line 39-50: Update the rate-limiting flow around enforceRateLimit so
anonymous requests must first pass an abuse control tied to a trusted requester
signal, such as verified IP/session or CAPTCHA, before consuming publisher
quotas. Preserve the existing domain limit as a secondary guard and retain the
global limit, ensuring untrusted callers cannot exhaust either quota.
In `@packages/backend/convex/reports.ts`:
- Around line 59-68: Update the rate limiting in the report handler around the
existing enforceRateLimit calls to key the primary limit by the individual
client or reporter rather than eventId, so one client cannot block reporting for
others. Retain contentReport:all only as a secondary global ceiling, and
preserve the existing limits and time window unless necessary for the per-client
key.
- Around line 177-225: Update the correction flow in the decision handler around
appendGenerationAudit and the nextStatus assignment so a correction remains
pending until summary regeneration and publication succeed. Do not set corrected
or schedule the outcome email immediately after enqueueing eventSummaryJobs;
instead, transition the report and notify the reporter from the successful
publication path, and unpublish the existing content while regeneration is
pending if supported by the existing status model.
In `@packages/backend/convex/sitemap.ts`:
- Around line 85-102: Give sources an independent reserved budget in the sitemap
query flow: define a fixed RESERVED_SOURCE_SLOTS value, cap the events query in
this block at limit minus that reservation, and fetch sources using the reserved
amount rather than events.length-based sourceLimit. Preserve non-negative limits
and ensure source pages remain included as events grow.
In `@packages/backend/convex/summarization.ts`:
- Around line 1290-1310: Update the review approval flow containing
appendGenerationAudit for action "review_approved" to include sourceArticles
derived from the associated review event, matching the published audit entry’s
provenance data. Reuse the existing event/source derivation mechanism and
preserve the current approval summary and metadata fields.
In `@packages/backend/convex/summarizationNode.ts`:
- Around line 866-888: The verifySummaryGrounding field-stripping logic must
restore fallback text when any non-neutral field becomes empty. In
verifySummaryGrounding, after filtering supported sentences, replace empty
reformist, suveranist, or globalImpact values with their corresponding original
field text before returning publish, while preserving the existing neutral
blocking behavior.
In `@packages/backend/convex/tdmPermissions.test.ts`:
- Around line 101-109: Add a regression assertion in the “noai meta and
X-Robots-Tag are detected” test for declaresNoAi, using a robots meta tag with
content before name and expecting true. Keep the existing detection and
negative-case assertions unchanged.
- Around line 64-93: Add regression coverage in the robots.txt tests around
evaluateTdmSignals, robotsDisallowsAll, and robotsCrawlDelay for duplicate
BiviantBot groups, ensuring a later BiviantBot Disallow: / is honored, and for
partial Allow rules, ensuring Disallow: / with Allow: /public still treats the
bot as restricted. Preserve the existing assertions for wildcard and
AI-convention tokens.
In `@packages/backend/convex/waitlist.ts`:
- Line 176: Update the waitlist unsubscribe flow around the return object in the
relevant mutation/function so token holders receive only the success status, or
a masked email rather than record.email. Adjust the route’s success message to
match the new response shape and remove any dependency on the full email field.
---
Outside diff comments:
In `@packages/backend/convex/summarization.ts`:
- Around line 890-955: The publish handler around the summary application logic
must fail closed when overlapCheck or grounding is absent, not only when a
provided check fails. Require both checks before allowing publication, returning
the appropriate rejection result for missing or failing validation; preserve any
legacy compatibility through a separate path rather than optional gates in this
handler.
In `@packages/backend/convex/waitlist.ts`:
- Around line 28-46: Update the waitlist mutation handler around consentRecord
so clientIp is not persisted as trusted consent provenance: obtain the IP from a
trusted server-side boundary, or rename and store it explicitly as
client-reported metadata. Validate consentSourcePage against the application’s
allowed paths before including it in the record, while preserving the existing
consent version, hash, and timestamp fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b97cf98f-c19b-4a0f-9ad1-204ac8593064
⛔ Files ignored due to path filters (2)
apps/web/src/routeTree.gen.tsis excluded by!**/routeTree.gen.tspackages/backend/convex/_generated/api.d.tsis excluded by!**/_generated/**,!**/_generated/**
📒 Files selected for processing (93)
.github/workflows/ci.ymlFOOTER_TODO.mdapps/web/e2e/compliance-event-page.spec.tsapps/web/e2e/consent-free-storage.spec.tsapps/web/src/components/early-access-apply-card.tsxapps/web/src/components/feed/ai-disclosure-label.test.tsxapps/web/src/components/feed/ai-disclosure-label.tsxapps/web/src/components/feed/articles-list.tsxapps/web/src/components/feed/event-card.test.tsxapps/web/src/components/feed/event-detail-tabs.tsxapps/web/src/components/feed/report-error-form.tsxapps/web/src/components/layout/Footer.tsxapps/web/src/components/layout/StaticPage.tsxapps/web/src/components/profile/AuthenticatedProfile.tsxapps/web/src/components/ui/snippet.test.tsxapps/web/src/components/ui/snippet.tsxapps/web/src/lib/getClientIp.tsapps/web/src/lib/posthog.tsxapps/web/src/lib/snippet.tsapps/web/src/routes/admin.publishers.tsxapps/web/src/routes/admin.reports.tsxapps/web/src/routes/admin.review.tsxapps/web/src/routes/bot.tsxapps/web/src/routes/contact.tsxapps/web/src/routes/cum-functioneaza.tsxapps/web/src/routes/despre.tsxapps/web/src/routes/event.$slug.tsxapps/web/src/routes/footer-pages.test.tsxapps/web/src/routes/politica-confidentialitate.tsxapps/web/src/routes/publishers.tsxapps/web/src/routes/source.$sourceId.tsxapps/web/src/routes/termeni.tsxapps/web/src/routes/unsubscribe.tsxdocs/bias-axis-spec.mddocs/compliance-image-policy.mddocs/compliance-storage-inventory.mddocs/compliance-third-party-text-audit.mdpackages/backend/convex/authMaintenance.tspackages/backend/convex/clustering.tspackages/backend/convex/compliance.test.tspackages/backend/convex/config.tspackages/backend/convex/consentHygiene.test.tspackages/backend/convex/crons.tspackages/backend/convex/dataRights.tspackages/backend/convex/domainPermissions.tspackages/backend/convex/domainPermissionsNode.tspackages/backend/convex/emails.tspackages/backend/convex/enrichment.tspackages/backend/convex/enrichmentNode.tspackages/backend/convex/events.tspackages/backend/convex/feeds.test.tspackages/backend/convex/feeds.tspackages/backend/convex/gdprCascade.test.tspackages/backend/convex/generationAudit.test.tspackages/backend/convex/generationAudit.tspackages/backend/convex/groundingGate.test.tspackages/backend/convex/imagePolicy.test.tspackages/backend/convex/ingestion.tspackages/backend/convex/lib/articleExtraction.tspackages/backend/convex/lib/botIdentity.tspackages/backend/convex/lib/compliance.tspackages/backend/convex/lib/consent.tspackages/backend/convex/lib/feedSerialization.tspackages/backend/convex/lib/grounding.tspackages/backend/convex/lib/imagePolicy.tspackages/backend/convex/lib/imageVerification.tspackages/backend/convex/lib/politeFetch.tspackages/backend/convex/lib/publicEventPreviews.tspackages/backend/convex/lib/retentionPolicy.tspackages/backend/convex/lib/tdmPolicy.tspackages/backend/convex/lib/verbatimOverlap.tspackages/backend/convex/migrations.tspackages/backend/convex/politeFetch.test.tspackages/backend/convex/prompts.tspackages/backend/convex/publisherRequests.test.tspackages/backend/convex/publisherRequests.tspackages/backend/convex/reports.test.tspackages/backend/convex/reports.tspackages/backend/convex/retention.test.tspackages/backend/convex/retention.tspackages/backend/convex/schema.tspackages/backend/convex/shareAssetsNode.tspackages/backend/convex/sitemap.tspackages/backend/convex/snippetEnforcement.test.tspackages/backend/convex/sourceReputation.tspackages/backend/convex/summarization.tspackages/backend/convex/summarizationNode.tspackages/backend/convex/summarizationPublishGate.test.tspackages/backend/convex/tdmPermissions.test.tspackages/backend/convex/verbatimOverlap.test.tspackages/backend/convex/waitlist.tspackages/backend/package.jsonpackages/i18n/src/strings.ts
💤 Files with no reviewable changes (3)
- apps/web/src/routes/despre.tsx
- packages/backend/convex/sourceReputation.ts
- packages/backend/convex/feeds.ts
| compliance-and-tests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Set persist-credentials: false on the checkout action.
By default actions/checkout@v4 persists the GITHUB_TOKEN in .git/config. Subsequent steps run pnpm install (which executes package install scripts) and vitest — both are untrusted-code paths that could exfiltrate the token.
🔒 Proposed fix
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - uses: actions/checkout@v4 | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 22-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 22-22: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml at line 22, Update the actions/checkout@v4 step in
the CI workflow to set persist-credentials to false, ensuring subsequent pnpm
install and vitest steps cannot access persisted GITHUB_TOKEN credentials.
Source: Linters/SAST tools
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: pnpm/action-setup@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Pin actions to SHA hashes instead of floating version tags.
All three actions (actions/checkout@v4, pnpm/action-setup@v4, actions/setup-node@v4) are pinned to major-version tags. If any action repository is compromised, a malicious update can be pushed under the same tag. Pin to a commit SHA and verify in dependabot.
🔒 Example pinning
- - uses: actions/checkout@v4
+ - uses: actions/checkout@<commit-sha> # v4
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@<commit-sha> # v4
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@<commit-sha> # v4🧰 Tools
🪛 zizmor (1.26.1)
[warning] 22-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 22-22: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 22 - 26, Replace the floating
major-version references in the workflow steps using actions/checkout,
pnpm/action-setup, and actions/setup-node with their verified full commit SHA
pins, retaining the existing action versions and configuration. Ensure
Dependabot recognizes and verifies the SHA-pinned dependencies.
Source: Linters/SAST tools
| {state === "sent" ? ( | ||
| <p className="text-sm text-muted-foreground">{t("report.sent")}</p> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add aria-live and role to status messages per coding guidelines.
The success message (line 60) and error message (line 126) are status messages that lack aria-live and role attributes. Screen reader users won't be notified when these appear. As per coding guidelines, forms must have ARIA live regions on status messages.
♿ Proposed fix for ARIA live regions
{state === "sent" ? (
- <p className="text-sm text-muted-foreground">{t("report.sent")}</p>
+ <p role="status" aria-live="polite" className="text-sm text-muted-foreground">
+ {t("report.sent")}
+ </p>
) : ( {state === "error" && (
- <p className="text-sm text-destructive">{t("report.error")}</p>
+ <p role="alert" aria-live="assertive" className="text-sm text-destructive">
+ {t("report.error")}
+ </p>
)}Also applies to: 125-127
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/feed/report-error-form.tsx` around lines 59 - 60,
Update the success message in the report form’s sent-state rendering and the
error message near the corresponding error-state rendering to include the
required ARIA live-region attributes, including aria-live and role. Preserve
their existing text, styling, translation keys, and conditional rendering.
Source: Coding guidelines
| const handleDeleteAccount = async () => { | ||
| if (!deleteArmed) { | ||
| setDeleteArmed(true); | ||
| toast.warning(t("profile.deleteConfirm")); | ||
| return; | ||
| } | ||
| setIsDeleting(true); | ||
| try { | ||
| await deleteMyAccount({}); | ||
| toast.success(t("profile.deleteDone")); | ||
| try { | ||
| await authClient.signOut({}); | ||
| } finally { | ||
| location.href = "/"; | ||
| } | ||
| } catch (error) { | ||
| console.error("Account deletion failed:", error); | ||
| toast.error(t("profile.deleteFailed")); | ||
| setIsDeleting(false); | ||
| setDeleteArmed(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Misleading "delete failed" toast when signOut throws after successful deletion.
deleteMyAccount and authClient.signOut share the same try/catch. If the mutation succeeds but signOut throws, the catch block fires toast.error(t("profile.deleteFailed")) even though the account was already deleted. For GDPR self-service, the user must receive accurate feedback about whether their data was erased — a false "failed" toast could prompt a retry that fails with "Not authenticated," causing confusion.
The existing handleSignOut (lines 120–133) already demonstrates the correct pattern: handle signOut errors independently. Apply the same separation here.
🔧 Proposed fix: separate deletion and signOut error handling
const handleDeleteAccount = async () => {
if (!deleteArmed) {
setDeleteArmed(true);
toast.warning(t("profile.deleteConfirm"));
return;
}
setIsDeleting(true);
try {
- await deleteMyAccount({});
- toast.success(t("profile.deleteDone"));
- try {
- await authClient.signOut({});
- } finally {
- location.href = "/";
- }
+ await deleteMyAccount({});
+ } catch (error) {
+ console.error("Account deletion failed:", error);
+ toast.error(t("profile.deleteFailed"));
+ setIsDeleting(false);
+ setDeleteArmed(false);
+ return;
+ }
+ toast.success(t("profile.deleteDone"));
+ try {
+ await authClient.signOut({});
} catch (error) {
- console.error("Account deletion failed:", error);
- toast.error(t("profile.deleteFailed"));
- setIsDeleting(false);
- setDeleteArmed(false);
+ console.error("Sign-out failed after deletion:", error);
+ } finally {
+ location.href = "/";
}
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleDeleteAccount = async () => { | |
| if (!deleteArmed) { | |
| setDeleteArmed(true); | |
| toast.warning(t("profile.deleteConfirm")); | |
| return; | |
| } | |
| setIsDeleting(true); | |
| try { | |
| await deleteMyAccount({}); | |
| toast.success(t("profile.deleteDone")); | |
| try { | |
| await authClient.signOut({}); | |
| } finally { | |
| location.href = "/"; | |
| } | |
| } catch (error) { | |
| console.error("Account deletion failed:", error); | |
| toast.error(t("profile.deleteFailed")); | |
| setIsDeleting(false); | |
| setDeleteArmed(false); | |
| } | |
| }; | |
| const handleDeleteAccount = async () => { | |
| if (!deleteArmed) { | |
| setDeleteArmed(true); | |
| toast.warning(t("profile.deleteConfirm")); | |
| return; | |
| } | |
| setIsDeleting(true); | |
| try { | |
| await deleteMyAccount({}); | |
| } catch (error) { | |
| console.error("Account deletion failed:", error); | |
| toast.error(t("profile.deleteFailed")); | |
| setIsDeleting(false); | |
| setDeleteArmed(false); | |
| return; | |
| } | |
| toast.success(t("profile.deleteDone")); | |
| try { | |
| await authClient.signOut({}); | |
| } catch (error) { | |
| console.error("Sign-out failed after deletion:", error); | |
| } finally { | |
| location.href = "/"; | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/profile/AuthenticatedProfile.tsx` around lines 97 -
118, Separate the error handling in handleDeleteAccount so the catch for
deleteMyAccount only reports deletion failures and resets deletion state. After
a successful deletion, handle authClient.signOut independently using the
existing handleSignOut pattern, while preserving the finally redirect to "/" and
avoiding any "profile.deleteFailed" toast when sign-out alone fails.
| const forwarded = getRequestHeader("x-forwarded-for"); | ||
| if (forwarded) { | ||
| return forwarded.split(",")[0]?.trim() ?? null; | ||
| } | ||
| return getRequestHeader("x-real-ip") ?? null; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Trace how getClientIp's return value flows into the waitlist mutation.
# Check the early-access-apply-card for how the IP is used
ast-grep outline apps/web/src/components/early-access-apply-card.tsx --items all --match 'getClientIp|waitlist'
# Search for where the IP is passed to the mutation
rg -n -C5 'getClientIp|clientIp|requesterIp' apps/web/src/components/early-access-apply-card.tsx
# Check the backend waitlist mutation for how the IP is stored/validated
rg -n -C5 'clientIp|requesterIp|ip' packages/backend/convex/waitlist.tsRepository: flvvius/news
Length of output: 2748
Don't persist clientIp from the browser
clientIp is user-controlled by the time it reaches the mutation, so this consent record can be forged. If the IP matters for provenance, capture it server-side from the request context; otherwise treat it as advisory only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/lib/getClientIp.ts` around lines 9 - 13, Stop using the
browser-provided result of getClientIp for persisted consent provenance, since
x-forwarded-for and x-real-ip can be forged by the client. Update the mutation
or consent-recording flow that consumes clientIp to obtain the address from
trusted server-side request context, or omit persistence and treat the value as
advisory only; do not rely on apps/web/src/lib/getClientIp.ts for authoritative
data.
| // L7: the approval (possibly edited = a correction of the proposal) is a | ||
| // new audit version superseding the held_for_review record. | ||
| await appendGenerationAudit(ctx, { | ||
| eventId: review.eventId, | ||
| jobId: review.jobId, | ||
| runId: review.runId, | ||
| action: "review_approved", | ||
| model: review.proposed.modelUsed, | ||
| promptVersion: String(SUMMARY_PROMPT_VERSION), | ||
| summary: { | ||
| neutral, | ||
| reformist, | ||
| suveranist, | ||
| globalImpact, | ||
| perspectiveApplicable: applicable, | ||
| }, | ||
| reviewOutcome: `approved by ${decidedByEmail ?? "admin"}${editedFields ? " (edited)" : ""}`, | ||
| disclosureLabelVersion: "v1", | ||
| publishedAt: Date.now(), | ||
| note, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Admin-approved corrections lose source provenance in the audit trail.
The published audit entry (Line 1033) includes sourceArticles: auditSources, but the review_approved audit entry appended here has no sourceArticles field at all. Since the module's stated goal is that "a single query reconstructs the chain: sources → checks → publication → corrections," human-reviewed/edited summaries silently lose the source-provenance portion of that chain.
🛡️ Proposed fix: derive source articles from the event before approving
+ const approvedArticles = await ctx.db
+ .query("articles")
+ .withIndex("by_event", (q) => q.eq("eventId", review.eventId))
+ .collect();
await appendGenerationAudit(ctx, {
eventId: review.eventId,
jobId: review.jobId,
runId: review.runId,
action: "review_approved",
model: review.proposed.modelUsed,
promptVersion: String(SUMMARY_PROMPT_VERSION),
summary: {
neutral,
reformist,
suveranist,
globalImpact,
perspectiveApplicable: applicable,
},
+ sourceArticles: approvedArticles.map((article) => ({
+ articleId: article._id,
+ canonicalUrl: article.canonicalUrl,
+ })),
reviewOutcome: `approved by ${decidedByEmail ?? "admin"}${editedFields ? " (edited)" : ""}`,
disclosureLabelVersion: "v1",
publishedAt: Date.now(),
note,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // L7: the approval (possibly edited = a correction of the proposal) is a | |
| // new audit version superseding the held_for_review record. | |
| await appendGenerationAudit(ctx, { | |
| eventId: review.eventId, | |
| jobId: review.jobId, | |
| runId: review.runId, | |
| action: "review_approved", | |
| model: review.proposed.modelUsed, | |
| promptVersion: String(SUMMARY_PROMPT_VERSION), | |
| summary: { | |
| neutral, | |
| reformist, | |
| suveranist, | |
| globalImpact, | |
| perspectiveApplicable: applicable, | |
| }, | |
| reviewOutcome: `approved by ${decidedByEmail ?? "admin"}${editedFields ? " (edited)" : ""}`, | |
| disclosureLabelVersion: "v1", | |
| publishedAt: Date.now(), | |
| note, | |
| }); | |
| // L7: the approval (possibly edited = a correction of the proposal) is a | |
| // new audit version superseding the held_for_review record. | |
| const approvedArticles = await ctx.db | |
| .query("articles") | |
| .withIndex("by_event", (q) => q.eq("eventId", review.eventId)) | |
| .collect(); | |
| await appendGenerationAudit(ctx, { | |
| eventId: review.eventId, | |
| jobId: review.jobId, | |
| runId: review.runId, | |
| action: "review_approved", | |
| model: review.proposed.modelUsed, | |
| promptVersion: String(SUMMARY_PROMPT_VERSION), | |
| summary: { | |
| neutral, | |
| reformist, | |
| suveranist, | |
| globalImpact, | |
| perspectiveApplicable: applicable, | |
| }, | |
| sourceArticles: approvedArticles.map((article) => ({ | |
| articleId: article._id, | |
| canonicalUrl: article.canonicalUrl, | |
| })), | |
| reviewOutcome: `approved by ${decidedByEmail ?? "admin"}${editedFields ? " (edited)" : ""}`, | |
| disclosureLabelVersion: "v1", | |
| publishedAt: Date.now(), | |
| note, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/backend/convex/summarization.ts` around lines 1290 - 1310, Update
the review approval flow containing appendGenerationAudit for action
"review_approved" to include sourceArticles derived from the associated review
event, matching the published audit entry’s provenance data. Reuse the existing
event/source derivation mechanism and preserve the current approval summary and
metadata fields.
| // Strip unsupported sentences; keep field text = supported sentences only. | ||
| const strippedFields: SummaryFields = { ...fields }; | ||
| for (const fieldName of [ | ||
| "neutral", | ||
| "reformist", | ||
| "suveranist", | ||
| "globalImpact", | ||
| ] as const) { | ||
| const fieldSentences = results.filter( | ||
| (entry) => entry.field === fieldName, | ||
| ); | ||
| if (fieldSentences.length === 0) continue; | ||
| strippedFields[fieldName] = fieldSentences | ||
| .filter((entry) => entry.supported) | ||
| .map((entry) => entry.sentence) | ||
| .join(" "); | ||
| } | ||
| if (!strippedFields.neutral.trim()) { | ||
| // The factual core itself is unsupported — nothing publishable remains. | ||
| return { action: "blocked", grounding: { ...grounding, passed: false } }; | ||
| } | ||
| return { action: "publish", fields: strippedFields, grounding }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'GLOBAL_IMPACT_FALLBACK|LIMITED_COVERAGE_FALLBACK|SIDE_COVERAGE_FALLBACK' packages/backend/convex/prompts.tsRepository: flvvius/news
Length of output: 513
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the grounding function and nearby logic.
wc -l packages/backend/convex/summarizationNode.ts
sed -n '820,910p' packages/backend/convex/summarizationNode.ts
# Inspect the fallback definitions in prompts.ts for exact behavior.
wc -l packages/backend/convex/prompts.ts
sed -n '110,170p' packages/backend/convex/prompts.ts
# Find where the published fields are later normalized or consumed.
rg -n 'perspectiveApplicable|globalImpact|reformist|suveranist|SummaryFields|grounding:|action: "publish"|action: "blocked"' packages/backend/convex -g '!**/dist/**'Repository: flvvius/news
Length of output: 44790
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the grounding helper used by the summarization pipeline.
wc -l packages/backend/convex/lib/grounding.ts
sed -n '1,180p' packages/backend/convex/lib/grounding.ts
# Inspect the CASE D / completeness logic in the summary pipeline.
sed -n '60,110p' packages/backend/convex/summarization.ts
# Inspect the publish path that persists the final summary fields.
sed -n '960,1040p' packages/backend/convex/summarization.tsRepository: flvvius/news
Length of output: 9969
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect tests around publish-gate handling of blank / partial summaries.
sed -n '1,260p' packages/backend/convex/summarizationPublishGate.test.ts
# Inspect the grounding-gate tests that exercise stripped fields.
sed -n '1,360p' packages/backend/convex/groundingGate.test.tsRepository: flvvius/news
Length of output: 18612
packages/backend/convex/summarizationNode.ts:866-888 — Grounding-stripped fields need fallback text before publish
verifySummaryGrounding only blocks when neutral becomes empty. If stripping empties reformist, suveranist, or globalImpact, the event can still publish with blank fields, which leaves the summary incomplete and eligible for re-summarization. Restore the existing fallback text for any emptied field before returning publish.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/backend/convex/summarizationNode.ts` around lines 866 - 888, The
verifySummaryGrounding field-stripping logic must restore fallback text when any
non-neutral field becomes empty. In verifySummaryGrounding, after filtering
supported sentences, replace empty reformist, suveranist, or globalImpact values
with their corresponding original field text before returning publish, while
preserving the existing neutral blocking behavior.
| test("robots.txt: our token, *, and AI-convention tokens", () => { | ||
| const robots = [ | ||
| "User-agent: *", | ||
| "Disallow: /admin", | ||
| "", | ||
| "User-agent: GPTBot", | ||
| "Disallow: /", | ||
| "", | ||
| "User-agent: BiviantBot", | ||
| "Crawl-delay: 5", | ||
| "Disallow:", | ||
| ].join("\n"); | ||
|
|
||
| // Our dedicated group allows crawling with a crawl delay. | ||
| expect(robotsDisallowsAll(robots, "BiviantBot")).toBe(false); | ||
| expect(robotsCrawlDelay(robots, "BiviantBot")).toBe(5); | ||
| // GPTBot fully disallowed → AI opt-out signal. | ||
| expect(robotsDisallowsAll(robots, "GPTBot")).toBe(true); | ||
|
|
||
| const evaluation = evaluateTdmSignals({ robotsTxt: robots }); | ||
| expect(evaluation.state).toBe("rss_only"); | ||
| expect(evaluation.signals).toContain("robots:gptbot"); | ||
| }); | ||
|
|
||
| test("robots.txt full * disallow restricts us too", () => { | ||
| const robots = "User-agent: *\nDisallow: /"; | ||
| const evaluation = evaluateTdmSignals({ robotsTxt: robots }); | ||
| expect(evaluation.state).toBe("rss_only"); | ||
| expect(evaluation.signals).toContain("robots:BiviantBot"); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- tdmPermissions.test.ts (relevant excerpt) ---'
sed -n '1,220p' packages/backend/convex/tdmPermissions.test.ts
echo
echo '--- tdmPolicy.ts outline ---'
ast-grep outline packages/backend/convex/lib/tdmPolicy.ts --view expanded || true
echo
echo '--- tdmPolicy.ts relevant excerpt ---'
sed -n '1,260p' packages/backend/convex/lib/tdmPolicy.tsRepository: flvvius/news
Length of output: 17157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate related tests and symbols ---'
rg -n "robotsDisallowsAll|robotsCrawlDelay|evaluateTdmSignals|Allow: /public|BiviantBot|GPTBot|merged groups|partial" packages/backend/convex -g '!**/node_modules/**'
echo
echo '--- list files near tdm policy ---'
fd -a "tdmPolicy.ts|tdmPermissions.test.ts|tdm" packages/backend/convex || trueRepository: flvvius/news
Length of output: 3839
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- test file with line numbers ---'
nl -ba packages/backend/convex/tdmPermissions.test.ts | sed -n '1,180p'
echo
echo '--- tdm policy file with line numbers ---'
nl -ba packages/backend/convex/lib/tdmPolicy.ts | sed -n '1,260p'Repository: flvvius/news
Length of output: 226
Add regressions for duplicate BiviantBot groups and partial Allow rules. A second BiviantBot group with Disallow: /, plus Disallow: / with Allow: /public, would catch first-match and overbroad-allow parsers that this file still misses.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/backend/convex/tdmPermissions.test.ts` around lines 64 - 93, Add
regression coverage in the robots.txt tests around evaluateTdmSignals,
robotsDisallowsAll, and robotsCrawlDelay for duplicate BiviantBot groups,
ensuring a later BiviantBot Disallow: / is honored, and for partial Allow rules,
ensuring Disallow: / with Allow: /public still treats the bot as restricted.
Preserve the existing assertions for wildcard and AI-convention tokens.
| test("noai meta and X-Robots-Tag are detected", () => { | ||
| expect( | ||
| declaresNoAi(`<meta name="robots" content="index, noai">`, undefined), | ||
| ).toBe(true); | ||
| expect(declaresNoAi(undefined, "noindex, noai")).toBe(true); | ||
| expect( | ||
| declaresNoAi(`<meta name="robots" content="index, follow">`, null), | ||
| ).toBe(false); | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Add a reversed-attribute noai regression.
Assert that <meta content="index, noai" name="robots"> is detected so valid attribute ordering cannot bypass the policy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/backend/convex/tdmPermissions.test.ts` around lines 101 - 109, Add a
regression assertion in the “noai meta and X-Robots-Tag are detected” test for
declaresNoAi, using a robots meta tag with content before name and expecting
true. Keep the existing detection and negative-case assertions unchanged.
| return { success: false as const }; | ||
| } | ||
| await ctx.db.patch(record._id, { status: "unsubscribed" }); | ||
| return { success: true as const, email: record.email }; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not return the subscriber’s full email to a token bearer.
A leaked unsubscribe token currently allows retrieval of the associated email address. Return only success, or a masked address, and update the route’s success message accordingly.
Proposed response change
- return { success: true as const, email: record.email };
+ return { success: true as const };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { success: true as const, email: record.email }; | |
| return { success: true as const }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/backend/convex/waitlist.ts` at line 176, Update the waitlist
unsubscribe flow around the return object in the relevant mutation/function so
token holders receive only the success status, or a masked email rather than
record.email. Adjust the route’s success message to match the new response shape
and remove any dependency on the full email field.
Summary by CodeRabbit