Skip to content

prg/main ahead 20260622 - #25

Merged
leoisadev1 merged 7 commits into
mainfrom
prg/main-ahead-20260622
Jun 22, 2026
Merged

prg/main ahead 20260622#25
leoisadev1 merged 7 commits into
mainfrom
prg/main-ahead-20260622

Conversation

@leoisadev1

Copy link
Copy Markdown
Member

Automated PR - Greptile CLI review workflow

@leoisadev1
leoisadev1 merged commit b053816 into main Jun 22, 2026
2 of 4 checks passed
@leoisadev1
leoisadev1 deleted the prg/main-ahead-20260622 branch June 22, 2026 15:00
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a large-scale "proactive pipeline" feature: a signal ingestion and clustering system that ingests GitHub/Discord/portal events, classifies them into customer needs, and generates draft notification/changelog proposals for human review before delivery. It also adds project-scoped public portals, changelog cover images, scheduled publishing, a tag management system, and an anonymous portal voting identity.

  • Proactive pipeline (pipeline.ts, agent.ts, proactiveClassifier.ts, proactiveProof.ts, schemaProactiveTables.ts): new event-to-need clustering flow with dedup via proactivePipelineEvents, memory-rule suppression, and person identity resolution — gated delivery via drafts.approve.
  • Changelog publish/schedule (amendContentMutationHandlers.ts, changelogScheduler.ts, crons.ts): explicit publish and schedule-for-later mutations with subscriber notification queuing; a 5-minute cron flips scheduled entries live.
  • Project-scoped portals (amendPortalReadHandlers.ts, amendSeed.ts): portal slug now resolves to a project first (with a take(2) collision guard), then falls back to a workspace slug.

Confidence Score: 3/5

The core ingest and dedup paths work, but two logic bugs in newly added code need fixing before the pipeline produces reliable data.

Two concrete logic defects are introduced in this PR. First, the pipeline scheduler in amendSourceIngest.ts fires with a permanently-undefined sourceEventId, so all evidence rows created through that path are disconnected from the source event record — the by_sourceEvent index is never populated for this flow, and the duplicate-evidence guard is silently skipped. Second, tags.ts has a case-sensitivity split: create deduplicates case-insensitively, while update uses an exact-case index, allowing 'Export' and 'export' to coexist as separate tags. Both are in newly written code on active paths.

packages/backend/convex/amendSourceIngest.ts (pipeline scheduler placement), packages/backend/convex/tags.ts (update clash check case sensitivity)

Security Review

  • apps/web/src/lib/sanitize-portal-html.ts: JS_URI_ATTR regex covers href, src, and xlink:href but not formaction, srcset, or poster — a <button formaction="javascript:..."> survives the strip. Low practical risk given the semi-trusted author model, but worth closing.
  • packages/backend/convex/ingest.ts: new HTTP endpoints (/ingest/githubWebhook, /ingest/discordWebhook, /ingest/sourceEvent) all require either a verified GitHub signature or a bearer token before processing — no unauthenticated write path.
  • packages/backend/convex/drafts.ts: safetyStrip redacts emails, AWS keys, Slack tokens, JWTs, and GitHub PATs from draft body text before queuing deliveries. The notification title field is not redacted, but it is sourced from the need title (max 80 chars) rather than raw user input.

Important Files Changed

Filename Overview
packages/backend/convex/amendSourceIngest.ts Pipeline scheduler call placed before sourceEventId is assigned — evidence rows created via this path will always have undefined sourceEventId.
packages/backend/convex/pipeline.ts New proactive signal pipeline — dedup logic is sound but the action wrapper is unnecessary overhead, and the source event ID linkage issue originates here.
packages/backend/convex/tags.ts New tag CRUD mutations — create uses case-insensitive client-side dedup while update uses a case-sensitive index, allowing near-duplicate tag names.
apps/web/src/lib/sanitize-portal-html.ts New regex-based sanitizer for changelog HTML — correctly applied via PortalProse, but JS_URI_ATTR misses formaction/srcset/poster attributes.
packages/backend/convex/drafts.ts New draft proposal flow with approve/reject/update — safetyStrip redaction and gated delivery queue look correct.
packages/backend/convex/ingest.ts New HTTP ingest endpoints for GitHub/Discord webhooks — both verify signatures/tokens before processing.
packages/backend/convex/changelogScheduler.ts New scheduled changelog publisher cron-driven every 5 minutes — correctly notifies subscribers on publish.
packages/backend/convex/schemaProactiveTables.ts New schema tables for the proactive pipeline — indexes look well-structured for the query patterns used.
packages/backend/convex/amendPortalReadHandlers.ts Portal now resolves project-scoped portals first — take(2)/length===1 collision guard is correct.
packages/backend/convex/amendContentMutationHandlers.ts New publish/schedule handlers for changelog — subscriber notification queue is correctly gated by notifySubscribers flag.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant W as Webhook / Portal
    participant I as ingest.ts
    participant SI as amendSourceIngest
    participant S as ctx.scheduler
    participant P as pipeline.processEvent
    participant CM as pipeline.commitProcessedEvent
    participant N as needs table
    participant E as evidence table
    participant D as draftProposals

    W->>I: POST /ingest/githubWebhook
    I->>I: verifyGitHubSignature / verifyApiToken
    I->>SI: trustedIngestSourceEvent (mutation)
    SI->>S: "runAfter(0, processEvent, sourceEventId=undefined)"
    SI->>SI: create sourceEvent, notification, reviewItem
    S-->>P: processEvent (action)
    P->>CM: commitProcessedEvent (mutation)
    CM->>CM: dedup check via proactivePipelineEvents
    CM->>N: findCompatibleNeed or insert new need
    CM->>E: "insert evidence (sourceEventId=undefined)"
    CM->>CM: recomputeNeedProof

    Note over D: Human review gate
    CM-->>D: (agent.ts) insert draftProposal on ship link
    D->>D: approve to queueNotificationDeliveries or queueChangelogReview
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant W as Webhook / Portal
    participant I as ingest.ts
    participant SI as amendSourceIngest
    participant S as ctx.scheduler
    participant P as pipeline.processEvent
    participant CM as pipeline.commitProcessedEvent
    participant N as needs table
    participant E as evidence table
    participant D as draftProposals

    W->>I: POST /ingest/githubWebhook
    I->>I: verifyGitHubSignature / verifyApiToken
    I->>SI: trustedIngestSourceEvent (mutation)
    SI->>S: "runAfter(0, processEvent, sourceEventId=undefined)"
    SI->>SI: create sourceEvent, notification, reviewItem
    S-->>P: processEvent (action)
    P->>CM: commitProcessedEvent (mutation)
    CM->>CM: dedup check via proactivePipelineEvents
    CM->>N: findCompatibleNeed or insert new need
    CM->>E: "insert evidence (sourceEventId=undefined)"
    CM->>CM: recomputeNeedProof

    Note over D: Human review gate
    CM-->>D: (agent.ts) insert draftProposal on ship link
    D->>D: approve to queueNotificationDeliveries or queueChangelogReview
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(proactive): handle scheduled publish..." | Re-trigger Greptile

Comment on lines 82 to +93

await ctx.scheduler.runAfter(0, internal.pipeline.processEvent, {
workspaceId: workspace._id,
sourceEventId,
externalId: args.externalId,
text: [args.title, args.labels?.join(" "), args.milestone].filter(Boolean).join("\n"),
title: args.title,
author: args.author,
url: args.url,
provider,
labels: args.labels ?? [],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 sourceEventId is always undefined when the pipeline is scheduled

The scheduler call at line 82 is placed immediately after let sourceEventId: Id<"sourceEvents"> | undefined; is declared (line 80), before the rest of trustedIngestSourceEventHandler ever assigns it. The variable is undefined at the call site every time. As a result, every evidence row created by the pipeline via this path will have sourceEventId: undefined, making the by_sourceEvent index unusable for pipeline-created evidence — and the existing-evidence dedup check in commitProcessedEvent skips the index lookup entirely (it guards with if (args.sourceEventId)), so the safeguard that prevents duplicate evidence rows is silently disabled for this call site.

Fix in Claude Code

Comment on lines +105 to +117
)
.first();
if (clash && clash._id !== tag._id) throw new Error("A tag with that name already exists.");
}

if (nextName !== tag.name) {
await rewriteTagAcrossEntries(ctx, workspace._id, tag.name, nextName);
}

await ctx.db.patch(tag._id, { name: nextName, color: nextColor, updatedAt: Date.now() });
return { id: tag._id, name: nextName, color: nextColor };
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Case-sensitivity inconsistency between create and update duplicate checks

create collects all workspace tags and does a toLowerCase() comparison, so "Export" and "export" are treated as duplicates. update, however, uses the by_workspace_and_name index with the exact string (nextName), which is a case-sensitive comparison. If a workspace already has a tag "Export", renaming a different tag to "export" passes the clash check (the index returns no result) and produces two near-identical tags that the UI will treat as distinct. The by_workspace_and_name check in update should either use a secondary case-folded lookup, or the create path should also rely on the index with the same casing rule.

Fix in Claude Code

Comment on lines +22 to +24
/[\s/]+(?:href|src|xlink:href)\s*=\s*(?:"\s*(?:javascript|data|vbscript):[^"]*"|'\s*(?:javascript|data|vbscript):[^']*'|\s*(?:javascript|data|vbscript):[^\s>]+)/gi;

export function sanitizePortalHtml(html: string | null | undefined): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security JS_URI_ATTR only covers href, src, and xlink:hrefsrcset, formaction, and poster are unchecked

Several other HTML attributes can hold URLs that some browsers will load or execute: srcset (images), formaction (buttons, bypasses stripped <form>), and poster (video). A <button formaction="javascript:..."> in authored content would survive the current regexes because the DANGEROUS_VOID strip removes <form> but not <button> or <input>. Given the semi-trusted author model noted in the file's own comment, this is low-risk in practice, but extending JS_URI_ATTR to include srcset|formaction|poster|action would close the gap without adding complexity.

Fix in Claude Code

Comment on lines +14 to +33
externalId: v.string(),
text: v.string(),
title: v.optional(v.string()),
author: v.optional(v.string()),
url: v.optional(v.string()),
provider: v.optional(v.string()),
labels: v.optional(v.array(v.string())),
email: v.optional(v.string()),
accountId: v.optional(v.string()),
},
returns: v.object({ ok: v.literal(true) }),
handler: async (ctx, args) => {
await ctx.runMutation(internal.pipeline.commitProcessedEvent, {
...args,
dedupeKey: `${args.workspaceId}:${args.externalId}`,
});
return { ok: true as const };
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 processEvent internalAction adds an async round-trip with no benefit

The processEvent handler's only body is await ctx.runMutation(internal.pipeline.commitProcessedEvent, { ...args, dedupeKey: ... }). An internalAction in Convex is warranted when you need to call external APIs or use capabilities unavailable in a mutation. Here there are none — all logic is in commitProcessedEvent. The extra action hop adds one more async step and one more potential point of failure in the scheduler queue. Moving the dedupeKey construction into commitProcessedEvent and calling it directly from ctx.scheduler.runAfter (as an internalMutation) would simplify the call graph without changing behaviour.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

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