Skip to content

Filter weak local campaign source units - #227

Open
lumenstech wants to merge 54 commits into
trycompai:mainfrom
lumenstech:local-lead-false-positive-filtering
Open

lumenstech wants to merge 54 commits into
trycompai:mainfrom
lumenstech:local-lead-false-positive-filtering

Conversation

@lumenstech

@lumenstech lumenstech commented Sep 17, 2026

Copy link
Copy Markdown

Summary

  • Adds deterministic false-positive filtering for weak local campaign source units.
  • Rejects event vendor links, website-only rows, navigation/footer/social links, registration/ticketing links, venue/organizer pages, and boilerplate.
  • Keeps ranking reasons separate from rejection reasons.
  • Prevents rejected units from being sent to Ollama.

Pilot Result

Compared with pilot-005 on the same 24-seed PartWall set:

  • Staged leads: 5 -> 4.
  • Weak website-only staged leads: 1 -> 0.
  • Useful named-company leads preserved: 4 -> 4.
  • Ollama calls: 17 -> 14.
  • Duplicates: 0.
  • Removed false positive: AutomateShow/AIMG.
  • Top rejection reasons: insufficient_company_evidence, weak_website_only, navigation_or_footer, boilerplate, registration_or_ticketing, venue_or_organizer.

Safety

  • No CRM writes.
  • No Neon writes.
  • No migrations.
  • No launch-agent edits.
  • No outreach or email sending.
  • deploy/ excluded.
  • Local pilot artifacts excluded.

Tests

  • Focused tests: 61 passed.
  • Type check: passed.
  • Biome: passed, with existing agent/lib/tasks.ts warning.
  • git diff --check: passed.
  • Campaign validation: passed.
  • --sync-crm refusal: passed.
  • Forbidden import scan: passed.

Next

After this, stop adding more local runner PRs for now. Run real lead batches and tune campaigns based on output quality.


Summary by cubic

Filters weak local campaign source units before they are sent to Ollama, reducing false-positive leads and Ollama calls. Adds a local campaign runner, a signal ingest API, and an Intelligence inbox for reviewing incoming signals.

Highlights

  • Rejects event vendor links, website-only rows, navigation/footer/social links, registration/ticketing, venue/organizer pages, and boilerplate.
  • Keeps ranking reasons separate from rejection reasons and records rejected units.
  • Local runner writes only to var/local-leads; no CRM, Neon, or launch-agent changes.
  • Adds ingest endpoints for signal submission, inbox, company resolution, and qualification.
  • Adds an Intelligence page with prefetch from the primary navigation.
  • Includes a database migration adding business units, canonical companies, and related columns; run migrations before deploying.

Written for commit 8186392. Summary will update on new commits.

Review in cubic

carhartlewis and others added 30 commits August 7, 2026 11:38
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Comp AI - PoC Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
github-actions Bot changed the base branch from release to main September 17, 2026 04:18
@github-actions

Copy link
Copy Markdown
Contributor

Retargeted this onto main.

release is the default branch so that a plain clone runs the last tagged release, but nothing merges into it — it is fast-forwarded onto the tag by the Release workflow and that is all. Changes go to main, and reach release when a release is cut.

Nothing is wrong with your branch. If the diff now shows commits that are already on main, rebase and force-push:

git fetch origin main
git rebase origin/main
git push --force-with-lease

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

40 issues found across 34 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/agent/agent/cli/local-campaign-worker.ts">

<violation number="1" location="apps/agent/agent/cli/local-campaign-worker.ts:23">
P1: When an option is absent, `argument()` returns the command instead of `undefined`. Normal `run`, `list`, and `export` invocations therefore fail while resolving omitted options. Return `undefined` when `index < 0`.</violation>

<violation number="2" location="apps/agent/agent/cli/local-campaign-worker.ts:68">
P1: CLI overrides bypass the campaign schema's Ollama and source-unit maxima. A command such as `--max-total-ollama-calls=100000` removes the configured call guardrail. Parse the merged campaign with `localCampaignSchema` before running.</violation>
</file>

<file name="apps/agent/agent/lib/local-lead-extraction.ts">

<violation number="1" location="apps/agent/agent/lib/local-lead-extraction.ts:105">
P1: When the model fabricates an evidence excerpt that is absent from the page, `extractLocalLead` still accepts and stages the lead. Pass the source text into validation and reject excerpts not found in it.</violation>

<violation number="2" location="apps/agent/agent/lib/local-lead-extraction.ts:136">
P1: The DNS preflight does not pin the address used by `fetch`, so DNS rebinding can bypass the local-address check. Use a resolver or HTTP dispatcher that validates and pins the connected address for the request.</violation>

<violation number="3" location="apps/agent/agent/lib/local-lead-extraction.ts:137">
P2: Every IPv6 source URL fails DNS validation because Node includes brackets in `parsed.hostname`, but `dns.lookup` expects the unbracketed address. Strip the brackets before lookup while retaining the original URL for `fetch`.</violation>

<violation number="4" location="apps/agent/agent/lib/local-lead-extraction.ts:154">
P1: A public source can redirect to `http://127.0.0.1/...`, and the default `fetch` follows that redirect without another DNS safety check. Disable automatic redirects or validate every `Location` target before following it.</violation>
</file>

<file name="packages/db/prisma/schema.prisma">

<violation number="1" location="packages/db/prisma/schema.prisma:1754">
P1: Mappings are global even though `canonicalId` points to business-unit-scoped records. A mapping in one business unit overwrites or marks the same source key as mapped in another; scope mappings by `businessUnitId` and every lookup.</violation>

<violation number="2" location="packages/db/prisma/schema.prisma:1782">
P2: Campaign members can target nonexistent canonical records because `companyId` and `personId` have no foreign keys. Add optional relations to `CanonicalCompany` and `CanonicalPerson`, then enforce the allowed target combinations.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-staging.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-staging.ts:26">
P2: On Windows or when the repository path contains spaces, `new URL(import.meta.url).pathname` is not a filesystem path. Use `fileURLToPath(import.meta.url)` before calling `dirname`.</violation>

<violation number="2" location="apps/agent/agent/lib/local-campaign-staging.ts:69">
P1: Every lead from the same source domain becomes `duplicate`, even when its company, contact, and content differ. Remove the standalone domain key; source-unit pages contain multiple legitimate companies.</violation>
</file>

<file name="apps/agent/agent/lib/local-ollama.ts">

<violation number="1" location="apps/agent/agent/lib/local-ollama.ts:55">
P1: When Ollama sends headers but stalls while streaming the body, `fetchImpl` resolves and this `finally` clears the timer before `response.json()` runs. Keep the abort timer active through status and body parsing, or race the entire fetch-and-parse operation, so a generation cannot exceed `per_ollama_call_timeout_ms` indefinitely.</violation>
</file>

<file name="apps/api/src/ingest/signal-qualification.service.ts">

<violation number="1" location="apps/api/src/ingest/signal-qualification.service.ts:129">
P1: When two promotion requests for the same signal run concurrently, both can pass `opportunityMapping` and create separate opportunities and deals before the mapping upsert resolves the race. Serialize or idempotently create promotion state under a transaction with a database-enforced uniqueness strategy, rather than relying on the final mapping upsert.</violation>

<violation number="2" location="apps/api/src/ingest/signal-qualification.service.ts:149">
P1: When `createDeal` validation or a later write fails, `promote` leaves the canonical opportunity behind because it persists it before validating the visible-deal path. Validate all prerequisites before writing, and wrap the promotion writes in an atomic transaction so failed promotions cannot create orphaned or partially updated records.</violation>
</file>

<file name="apps/api/src/ingest/ingest.service.ts">

<violation number="1" location="apps/api/src/ingest/ingest.service.ts:96">
P2: When a caller supplies `entity` or `signal_score` inside `payload` and omits the optional top-level field, these assignments replace the supplied value with `null`. Preserve payload values unless the corresponding top-level input was explicitly provided.</violation>

<violation number="2" location="apps/api/src/ingest/ingest.service.ts:139">
P1: When an accepted payload contains a nonnumeric score string, PostgreSQL throws during the inbox query before returning any rows. Guard the casts with numeric validation, or normalize and validate score values at ingest, in every score expression.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx:216">
P1: When a reviewer opens another signal, `SignalReviewSheet` reuses the previous signal's score fields and owner. Key the sheet by `sourceRecordId` or reset both states when `row` changes.</violation>

<violation number="2" location="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx:217">
P1: After resolving or qualifying a signal, the open sheet still shows the old company and score, leaving promotion disabled or incorrectly enabled. Derive the sheet row from the refreshed `rows` by `sourceRecordId` and close it when the selected row leaves the current view.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-source-units.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-source-units.ts:146">
P2: When a page contains only event-level text and its own event URL, `hasUsefulPageEvidence` treats that URL as company evidence and the runner sends the whole page to Ollama. Ignore the parent event URL for this check or require company-specific evidence before allowing page-level fallback.</violation>

<violation number="2" location="apps/agent/agent/lib/local-campaign-source-units.ts:255">
P2: When the same company appears in a row and an anchor with different surrounding text, the differing `content_hash` makes this composite key unique despite matching website or company identity. Use the first available stable identity key, falling back to the hash only when neither website nor company is available, so duplicate units do not consume separate Ollama calls.</violation>

<violation number="3" location="apps/agent/agent/lib/local-campaign-source-units.ts:284">
P1: When a normal table or list row exceeds 120 characters, `candidateCompanyName` sees the flattened row as one oversized segment and returns `null`. Preserve block or cell boundaries, or extract the company name before applying the segment-length limit, so useful named-company rows are not rejected as `weak_website_only` or `insufficient_company_evidence`.</violation>

<violation number="4" location="apps/agent/agent/lib/local-campaign-source-units.ts:429">
P2: When a company-labeled utility anchor uses `javascript:`, `tel:`, or `data:`, `normalizeWebsite` returns it as `candidate_website` and the rejection logic counts it as evidence. Accept only `http:` and `https:` URLs before passing the value into website and evidence checks.</violation>
</file>

<file name="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql">

<violation number="1" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:11">
P1: On a fresh deployment, this migration leaves `business_unit` empty, so `IngestService.signal()` rejects every project as unknown. Seed the required business units or add a provisioning path before enabling ingestion.</violation>

<violation number="2" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:234">
P1: When two business units use the same source identity, this global index makes the later ingest overwrite the first unit's `businessUnitId` and payload. Include `businessUnitId` in the source identity and update the lookup and conflict target accordingly.</violation>

<violation number="3" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:249">
P2: Because `companyId` and `personId` are nullable, PostgreSQL allows duplicate company-only or person-only members despite this unique index. Use `NULLS NOT DISTINCT` or equivalent partial unique indexes for the intended deduplication.</violation>

<violation number="4" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:291">
P1: These independent foreign keys allow an opportunity in one business unit to reference a company in another. Enforce matching `businessUnitId` values with composite tenant-scoped keys.</violation>
</file>

<file name="apps/api/src/ingest/ingest.contracts.ts">

<violation number="1" location="apps/api/src/ingest/ingest.contracts.ts:30">
P1: This accepts `javascript:` and `data:` URLs. Clicking a shared signal can execute attacker-controlled content. Restrict `sourceUrl` to HTTP and HTTPS URLs.</violation>

<violation number="2" location="apps/api/src/ingest/ingest.contracts.ts:104">
P2: When a caller sends `domain: ""`, `resolveCompany` ignores the signal domain and can create a duplicate company without a domain. Reject blank domains with `.min(1)`.</violation>

<violation number="3" location="apps/api/src/ingest/ingest.contracts.ts:155">
P2: When promotion creates a deal, `amountUsd` bypasses the CRM amount ceiling and can create an out-of-range amount or fail during persistence. Apply the shared dollar cap to this input and the payload fallback before `deals.create`.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-cache.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-cache.ts:30">
P2: When `pages.jsonl` contains one truncated or invalid JSON line, `readCachedPage` throws before checking the requested URL, causing every seed to fail as a fetch error. Skip malformed cache lines and refetch that URL instead of treating cache corruption as a page-fetch failure.</violation>

<violation number="2" location="apps/agent/agent/lib/local-campaign-cache.ts:43">
P1: A seed hangs indefinitely when its server stalls after response headers. Keep the fetch deadline active while reading the body.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-schema.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-schema.ts:13">
P2: When a campaign declares only `search`, the schema reports it valid but the runner has no search path, so the run can silently process zero seeds. Reject `search` until the runner implements it, or wire this source type to an actual search input.</violation>

<violation number="2" location="apps/agent/agent/lib/local-campaign-schema.ts:29">
P2: When `required_fields` contains an unknown lead field, validation succeeds and the runner marks that requirement as satisfied because `undefined !== null`. Restrict `required_fields` to fields produced by the local lead schema.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-runner.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-runner.ts:461">
P2: When a fetch or extraction takes longer than `perSeedTimeoutMs`, this check is reached only after the operation has already completed, and the lead can still be staged. Enforce the deadline around each awaited stage and mark the seed timed out without processing its result.</violation>

<violation number="2" location="apps/agent/agent/lib/local-campaign-runner.ts:474">
P2: When schema repair is needed, one extraction invokes Ollama twice but this counter records one call, so `max_total_ollama_calls`, per-seed limits, progress, and `ollama_calls` undercount actual requests. Count each generation attempt and enforce the budget before retries.</violation>
</file>

<file name="apps/api/src/ingest/ingest.module.ts">

<violation number="1" location="apps/api/src/ingest/ingest.module.ts:9">
P1: Because `IngestModule` does not import `TrpcModule`, its `@Router` cannot resolve the `AuthMiddleware` attached by `@UseMiddlewares`, so the ingest procedures will fail to register or the application will fail during router initialization. Import `TrpcModule` directly in this module.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-extraction.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-extraction.ts:154">
P2: When a model puts a non-null scalar outside `evidence_excerpt`, the extractor fails without its one repair attempt. Include scalar-evidence validation errors in `needsSchemaRepair` so this path can be repaired consistently.</violation>
</file>

<file name="apps/agent/agent/lib/local-lead-staging.ts">

<violation number="1" location="apps/agent/agent/lib/local-lead-staging.ts:17">
P2: Symlinked `var/local-leads` components bypass this guard and redirect `readFile` and `appendFile` outside the staging directory. Resolve existing components with `realpath` or reject symlinks before writing.</violation>
</file>

<file name="apps/agent/campaigns/partwall-2027.json">

<violation number="1" location="apps/agent/campaigns/partwall-2027.json:4">
P2: The campaign declares customer, geography, event-window, and source-type constraints, but the runner never applies them. Out-of-scope companies can therefore still become staged leads. Apply these constraints during extraction or filtering, or remove the misleading fields.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-lead-schema.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-lead-schema.ts:6">
P2: When a table or list source unit contains a bare `www.…` website, the extractor preserves that value, but this `.url()` check rejects it because it is not an absolute URL. Normalize bare domains before validation or use the same non-URL website contract as the existing local lead schema, otherwise valid company rows can be dropped after the repair retry.</violation>
</file>

<file name="apps/agent/agent/lib/local-campaign-scoring.ts">

<violation number="1" location="apps/agent/agent/lib/local-campaign-scoring.ts:21">
P2: When the model puts a campaign keyword or exclusion only in `project_fit_reason`, this scorer changes `fit_score` without source evidence. Match only source-backed fields so generated rationale cannot create or remove campaign fit.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

return (
args
.find((value) => value.startsWith(`${flag}=`))
?.slice(flag.length + 1) ?? args[index + 1]

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When an option is absent, argument() returns the command instead of undefined. Normal run, list, and export invocations therefore fail while resolving omitted options. Return undefined when index < 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/cli/local-campaign-worker.ts, line 23:

<comment>When an option is absent, `argument()` returns the command instead of `undefined`. Normal `run`, `list`, and `export` invocations therefore fail while resolving omitted options. Return `undefined` when `index < 0`.</comment>

<file context>
@@ -0,0 +1,126 @@
+	return (
+		args
+			.find((value) => value.startsWith(`${flag}=`))
+			?.slice(flag.length + 1) ?? args[index + 1]
+	);
+}
</file context>
Fix with cubic

let response: Response;
try {
response = await Promise.race([
fetchImpl(url, {

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: A public source can redirect to http://127.0.0.1/..., and the default fetch follows that redirect without another DNS safety check. Disable automatic redirects or validate every Location target before following it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-lead-extraction.ts, line 154:

<comment>A public source can redirect to `http://127.0.0.1/...`, and the default `fetch` follows that redirect without another DNS safety check. Disable automatic redirects or validate every `Location` target before following it.</comment>

<file context>
@@ -0,0 +1,301 @@
+	let response: Response;
+	try {
+		response = await Promise.race([
+			fetchImpl(url, {
+				signal: controller.signal,
+				headers: { accept: "text/html,text/plain;q=0.9" },
</file context>
Fix with cubic

].join("\n");

const result = await client.generate({ model: LOCAL_LEAD_MODEL, prompt });
const lead = parseLocalLeadResponse(result.text, sourceUrl);

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When the model fabricates an evidence excerpt that is absent from the page, extractLocalLead still accepts and stages the lead. Pass the source text into validation and reject excerpts not found in it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-lead-extraction.ts, line 105:

<comment>When the model fabricates an evidence excerpt that is absent from the page, `extractLocalLead` still accepts and stages the lead. Pass the source text into validation and reject excerpts not found in it.</comment>

<file context>
@@ -0,0 +1,301 @@
+	].join("\n");
+
+	const result = await client.generate({ model: LOCAL_LEAD_MODEL, prompt });
+	const lead = parseLocalLeadResponse(result.text, sourceUrl);
+	const contentHash = createHash("sha256").update(text, "utf8").digest("hex");
+
</file context>
Fix with cubic

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([sourceSystem, sourceType, sourceId, canonicalType])

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Mappings are global even though canonicalId points to business-unit-scoped records. A mapping in one business unit overwrites or marks the same source key as mapped in another; scope mappings by businessUnitId and every lookup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/prisma/schema.prisma, line 1754:

<comment>Mappings are global even though `canonicalId` points to business-unit-scoped records. A mapping in one business unit overwrites or marks the same source key as mapped in another; scope mappings by `businessUnitId` and every lookup.</comment>

<file context>
@@ -1583,3 +1592,249 @@ model Apikey {
+  createdAt      DateTime @default(now())
+  updatedAt      DateTime @updatedAt
+
+  @@unique([sourceSystem, sourceType, sourceId, canonicalType])
+  @@index([canonicalType, canonicalId])
+  @@map("record_mapping")
</file context>
Fix with cubic


function dedupeKeys(lead: LocalStagedLead): string[] {
return [
`domain:${normalizeDomain(lead.source_domain)}`,

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Every lead from the same source domain becomes duplicate, even when its company, contact, and content differ. Remove the standalone domain key; source-unit pages contain multiple legitimate companies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-staging.ts, line 69:

<comment>Every lead from the same source domain becomes `duplicate`, even when its company, contact, and content differ. Remove the standalone domain key; source-unit pages contain multiple legitimate companies.</comment>

<file context>
@@ -0,0 +1,118 @@
+
+function dedupeKeys(lead: LocalStagedLead): string[] {
+	return [
+		`domain:${normalizeDomain(lead.source_domain)}`,
+		lead.company_name ? `company:${normalizeText(lead.company_name)}` : null,
+		lead.email ? `email:${normalizeText(lead.email)}` : null,
</file context>
Fix with cubic

export const localCampaignLeadSchema = z
.object({
company_name: z.string().trim().min(1).nullable(),
website: z.string().trim().url().nullable(),

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When a table or list source unit contains a bare www.… website, the extractor preserves that value, but this .url() check rejects it because it is not an absolute URL. Normalize bare domains before validation or use the same non-URL website contract as the existing local lead schema, otherwise valid company rows can be dropped after the repair retry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-lead-schema.ts, line 6:

<comment>When a table or list source unit contains a bare `www.…` website, the extractor preserves that value, but this `.url()` check rejects it because it is not an absolute URL. Normalize bare domains before validation or use the same non-URL website contract as the existing local lead schema, otherwise valid company rows can be dropped after the repair retry.</comment>

<file context>
@@ -0,0 +1,29 @@
+export const localCampaignLeadSchema = z
+	.object({
+		company_name: z.string().trim().min(1).nullable(),
+		website: z.string().trim().url().nullable(),
+		contact_name: z.string().trim().min(1).nullable(),
+		role: z.string().trim().min(1).nullable(),
</file context>
Suggested change
website: z.string().trim().url().nullable(),
website: z.string().trim().min(1).nullable(),
Fix with cubic


export function hasUsefulPageEvidence(text: string): boolean {
const cleaned = cleanText(text);
return looksCompanyLevel(cleaned) && !isOnlyEventLevel(cleaned);

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When a page contains only event-level text and its own event URL, hasUsefulPageEvidence treats that URL as company evidence and the runner sends the whole page to Ollama. Ignore the parent event URL for this check or require company-specific evidence before allowing page-level fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-source-units.ts, line 146:

<comment>When a page contains only event-level text and its own event URL, `hasUsefulPageEvidence` treats that URL as company evidence and the runner sends the whole page to Ollama. Ignore the parent event URL for this check or require company-specific evidence before allowing page-level fallback.</comment>

<file context>
@@ -0,0 +1,460 @@
+
+export function hasUsefulPageEvidence(text: string): boolean {
+	const cleaned = cleanText(text);
+	return looksCompanyLevel(cleaned) && !isOnlyEventLevel(cleaned);
+}
+
</file context>
Fix with cubic

lead.company_name,
lead.website,
lead.location,
lead.project_fit_reason,

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When the model puts a campaign keyword or exclusion only in project_fit_reason, this scorer changes fit_score without source evidence. Match only source-backed fields so generated rationale cannot create or remove campaign fit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-scoring.ts, line 21:

<comment>When the model puts a campaign keyword or exclusion only in `project_fit_reason`, this scorer changes `fit_score` without source evidence. Match only source-backed fields so generated rationale cannot create or remove campaign fit.</comment>

<file context>
@@ -0,0 +1,91 @@
+		lead.company_name,
+		lead.website,
+		lead.location,
+		lead.project_fit_reason,
+		lead.evidence_excerpt,
+	]
</file context>
Fix with cubic

export const resolveSignalCompanyInput = signalSourceRecordInput.extend({
companyId: z.string().trim().min(1).nullable().optional(),
companyName: z.string().trim().min(1).max(320).nullable().optional(),
domain: z.string().trim().max(320).nullable().optional(),

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When a caller sends domain: "", resolveCompany ignores the signal domain and can create a duplicate company without a domain. Reject blank domains with .min(1).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/ingest/ingest.contracts.ts, line 104:

<comment>When a caller sends `domain: ""`, `resolveCompany` ignores the signal domain and can create a duplicate company without a domain. Reject blank domains with `.min(1)`.</comment>

<file context>
@@ -0,0 +1,186 @@
+export const resolveSignalCompanyInput = signalSourceRecordInput.extend({
+	companyId: z.string().trim().min(1).nullable().optional(),
+	companyName: z.string().trim().min(1).max(320).nullable().optional(),
+	domain: z.string().trim().max(320).nullable().optional(),
+	createIfMissing: z.boolean().default(false),
+	queueResearch: z.boolean().default(true),
</file context>
Suggested change
domain: z.string().trim().max(320).nullable().optional(),
domain: z.string().trim().min(1).max(320).nullable().optional(),
Fix with cubic

}
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
const row = JSON.parse(line) as CachedPage;

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When pages.jsonl contains one truncated or invalid JSON line, readCachedPage throws before checking the requested URL, causing every seed to fail as a fetch error. Skip malformed cache lines and refetch that URL instead of treating cache corruption as a page-fetch failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-cache.ts, line 30:

<comment>When `pages.jsonl` contains one truncated or invalid JSON line, `readCachedPage` throws before checking the requested URL, causing every seed to fail as a fetch error. Skip malformed cache lines and refetch that URL instead of treating cache corruption as a page-fetch failure.</comment>

<file context>
@@ -0,0 +1,55 @@
+	}
+	for (const line of raw.split("\n")) {
+		if (!line.trim()) continue;
+		const row = JSON.parse(line) as CachedPage;
+		if (row.url === url) return row;
+	}
</file context>
Fix with cubic

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.

2 participants