Replies: 6 comments
|
Branch: Tier 0 fixes shipped in PR #4020 — covers FactBase numeric coercion, proposed_claims entityId validation, source check verdict storage error propagation, and factbase-source-check resource resolution. |
|
Branch: Phase A continuation shipped in PR #4023 — adds the validate-dangerous-patterns gate check (silent .catch, warn-only .catch on data writes, as any in routes, skipEntityValidation without reason) and hardens skipEntityValidation to require a justification at every layer (server logs, client wrapper throws, CLI flag enforces). Phase A is now complete. |
|
Branch: Phase B sub-PRs 1-3 shipped: PR #4029 (B1: validateEntityRefs on 8 endpoints), PR #4030 (B2: CHECK constraints on 14 status fields), PR #4032 (B3: unique indexes on natural keys for grants/funding_rounds/policy_stakeholders). Phase B4 (onDelete policy) and B5 (JSONB Zod validation) remain. |
|
Branch: Phase B complete — all 5 sub-PRs open and CI green:
Phase A+B now covers: Tier 0 active-corruption fixes, prevention gate, skipEntityValidation hardening, FK validation on all sync endpoints, CHECK constraints, natural-key uniqueness, FK delete policy, and JSONB schema for the highest-value column. Remaining epic items (Tiers 2-7) are tracked separately. |
|
Branch: Phase C complete — all 3 sub-PRs open and ready for review:
Phase A (prevention) + Phase B (schema) + Phase C (bypass elimination) are now complete. Remaining: Phase D (concurrency/pipeline) and Phase E (cache drift monitoring). |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
A broad investigation triggered by the
resourceId: nullevidence bug found that this was not an isolated issue. There are 15 distinct categories of data integrity gaps across the codebase, falling into one underlying pattern: validation is opt-in, bypass is one line away, and "best-effort" is the default for what is actually primary data.This epic tracks the work to systematically close those gaps.
Why This Matters
Several findings represent active data corruption happening right now, not theoretical risks:
entityId = NULL, creating orphaned verification workresourceId: nullbug repeats in 8 other code pathsThe root cause is cultural, not local: a single fix will not move the needle. We need a coordinated push across schema, validation, bypass paths, and the build pipeline.
Investigation Findings
Full investigation in [internal notes — link TBD]. Summary of categories:
Tier 0 — Active data corruption (P0)
row.numeric ?? 0)apps/wiki-server/src/routes/factbase/facts.ts:83,95,97proposed_claimsinserts allowentityId = NULLapps/wiki-server/src/routes/claims/claims.ts:198,231.catch(() => warn)on primary writescrux/commands/source-check-wiki-pages.ts:366,434,481,514;crux/lib/job-handlers/claim-verification.ts:320-337resourceId: nullbug + 8 similar bypass paths in evidence creationcrux/lib/source-check/verdict-handler.ts:42-44;crux/commands/factbase-source-check.ts:233Tier 1 — Schema-level enforcement gaps
grants.ts:675,divisions.ts:251,division-personnel.ts:187,entity-assessments.ts:141,equity-positions.ts:260,policy-stakeholders.ts:133,things.ts:590,research-areas.ts:507,596,entity-resources.ts:92. ThevalidateEntityRefs()helper exists and is used byinvestments.ts,personnel.ts,benchmark-results.ts,citations.ts— pure inconsistency.jobs.status,auto_update_results.status,sourceCheckVerdicts.verdict,grants.status,divisions.status,funding_programs.status,facts.verdict,resources.enrichmentStatus,resources.fetchStatus,incidents.status,political_races.status,political_candidates.status,agent_sessions.status,research_areas.status.onDelete: "set null"on critical FKs silently orphans records:citation_quotes.resourceId,pageCitations.resourceId,statements.propertyId,statements.valueEntityId,personnel.{personEntityId,orgEntityId},grants.{orgEntityId,granteeEntityId}.personnel(personEntityId, orgEntityId, roleType, startDate),grants(orgEntityId, granteeEntityId, round, amount),investments(...),funding_rounds(...),policy_stakeholders(...). Concurrent requests can create duplicates the application thinks were prevented.Tier 2 — Concurrency and atomicity bugs
dualWriteToSourceCheck()inapps/wiki-server/src/routes/wikibase/citations.ts:48-167writes tosourceCheckEvidenceandsourceCheckVerdictsin sequence, called fire-and-forget at line 649. Tables can become permanently inconsistent.apps/wiki-server/src/routes/operational/monitoring.ts:351-399.apps/wiki-server/src/routes/tablebase/entities.ts:930-964.apps/wiki-server/src/routes/tablebase/ids.ts:105-145callsnextval()thenonConflictDoNothing(). Concurrent allocations burn sequence values.apps/wiki-server/src/routes/wikibase/pages.ts:256-279.Tier 3 — Type safety bypasses hiding data drift
crux/lib/anthropic.ts:189-212,crux/lib/json-parsing.ts:82-150. Truncated responses get regex-extracted partial JSON, then cast as the expected type.monitoring.ts:525,543,github-issues.ts:65,crux/lib/github.ts:131,214), OpenRouter (crux/lib/llm.ts:199-204), Wikidata, ProPublica, Bluesky, Semantic Scholar, Exa.crux/auto-update/feed-fetcher.ts:131-188. Hand-rolled XML parsing with broken entity handling.z.record(z.unknown())and read with no validation:jobs.{params,result},page_improve_runs.{citationAudit,qualityMetrics},tablebase_audit_log.{oldData,newData},auto_update_runs.newPagesCreated(with a CSV-fallback hack atauto-update-runs.ts:83-91),active_agents.{metadata,filesTouched},data_sources.verificationConfig,claims.{resourceIds,qualifiers}.Tier 4 — Cached/denormalized data drift
things.verdictpopulated at migration time and never updated:apps/wiki-server/drizzle/0087_populate_things_from_domain_tables.sql:203-221. Search results show stale verdicts.research_area_scoresonly updated by manual recompute:apps/wiki-server/src/routes/tablebase/research-areas.ts:850-929.apps/wiki-server/src/routes/operational/build-metrics.ts:146-247syncs fire-and-forget; partial syncs leave metrics inconsistent across pages.Tier 5 — Bypass paths and inconsistent enforcement
skipEntityValidation=trueis a mainline feature:crux/lib/wiki-server/personnel.ts:59-66. Used bynormalize-ids.tsandsync-careers-to-personnel.ts.apiRequest()calls bypass typed clients:factbase-source-check.ts:233,verify-entity.ts:26,verify-stakeholders.ts:27-28,tablebase.ts:628,import-quri-personnel.ts,claim-verification.ts:223.grants.ts:770-787,personnel.ts:543-557,investments.ts:381-396,benchmark-results.ts:281-296,funding-rounds.ts:385-400. Records committed; claim links fail silently; caller can't tell.crux/wiki-server/sync-common.ts:300-310.Tier 6 — Read-time enrichment hiding write-time gaps
The codebase is full of fallback chains like
entityTitle ?? (displayName && !isSid(displayName) ? displayName : null) ?? (rawId && !isSid(rawId) ? rawId : null)— a sign that none of the layers can be trusted so the read code tries them all. Locations:entity-ref.ts:36-41, personnel, funding-rounds, investments, grants, things, record-lookup.Tier 7 — Build pipeline fail-open behavior
apps/web/scripts/build-data.mjs:191-193,1309-1315.database.jsonis written first, then the script exits non-zero — too late.apps/web/scripts/lib/wiki-server-data.mjs:75-89,303-312.buildEditLogDateMap(),fetchAssessments(),fetchResourcesFromPG()return empty maps on error; build still succeeds.readdirSync()inpackages/factbase/data/things/. Different machines build different databases.The Underlying Pattern
This is not 15 unrelated bugs. It's one engineering culture problem showing up in 15 places:
skipEntityValidation=true, rawapiRequest(),as Tcasts)$type<string[]>(),as T, hand-written interfaces for external APIs)onDelete: set nulleverywhere)A single PR will not fix this. It needs a coordinated push.
Proposed Path Forward
Phase A — Stop the bleeding (week 1)
.catch(() => {}),.catch(() => console.warn(...))on data writes,as anyin route files,?? nullon FK columns,JSON.parsewithout subsequent Zod validationskipEntityValidation=truefail loudly without an explicit suppression commentproposed_claimsinserts with NULLentityId.catch()on evidence/verdict storage with proper error propagationfactbase-source-check.tsthroughstoreSourceCheckEvidence()instead of bypassing itPhase B — Tighten the schema (weeks 2-3, one PR per category)
onDelete: "set null"withrestrictorcascadebased on intent — every one needs an explicit decisionvalidateEntityRefs()calls to the 9 endpoints that don't have themPhase C — Eliminate bypass paths (weeks 3-4, one PR per data type)
storeSourceCheckEvidence(); deletestoreEvidenceApidirect callerssyncPersonnel()with validation always onstoreClaimVerdict()wrapper; delete the rawapiRequestpathdualWriteToSourceCheck()in a transaction; stop calling it fire-and-forgetPhase D — Concurrency and pipeline (weeks 4-5)
INSERT ... ON CONFLICT+ unique indexids.tsto avoid burning sequence valuesdatabase.json; wiki-server unreachable fails the build (with explicit--allow-staleoverride); snapshots > 24h old fail the buildPhase E — Catch the cached drift (week 5)
things.verdictwhen source verdicts changeresearch_area_scoreson evaluation insertOut of Scope
Success Criteria
.catch(() => {})or equivalent on primary data writes (gate-enforced)z.record(z.unknown())for JSONB columns that have known shapesRelated Verification Subsystem Discussions
The verification / source-check subsystem is a major sub-area of this data integrity epic. The post-cleanup canonical surviving discussions in that cluster (per the 2026-04-08 discussions review) are:
Adjacent (different scope but related):
Discussions closed as superseded on 2026-04-08: #3567, #3586, #3741, #3875, #3880, #3930.
How to Help
If you're picking up a sub-issue from this epic, read the underlying file first — the line numbers above are accurate as of investigation time but may have shifted. The pattern matters more than the exact line.
Tasks
Tier 0 data integrity fixes (epic #4017) #4020 — Tier 0 data integrity fixes (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
Phase A continuation: gate check + skipEntityValidation hardening (epic #4017) #4023 — Phase A continuation: gate check + skipEntityValidation hardening (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B1: Add validateEntityRefs to 8 missing sync endpoints (epic #4017) #4029 — B1: Add validateEntityRefs to 8 missing sync endpoints (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B2: Add CHECK constraints on 14 status/enum columns (epic #4017) #4030 — B2: Add CHECK constraints on 14 status/enum columns (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B3: Add unique indexes on natural keys — grants, funding_rounds, policy_stakeholders (epic #4017) #4032 — B3: Add unique indexes on natural keys — grants, funding_rounds, policy_stakeholders (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B4: Tighten onDelete policy — 6 critical FKs from SET NULL to RESTRICT (epic #4017) #4033 — B4: Tighten onDelete policy — 6 critical FKs from SET NULL to RESTRICT (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B5: Add typed Zod schema for verificationConfig JSONB column (epic #4017) #4034 — B5: Add typed Zod schema for verificationConfig JSONB column (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
C1-C3: Eliminate 6 bypass callers — route through typed wrappers (epic #4017) #4038 — C1-C3: Eliminate 6 bypass callers — route through typed wrappers (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
C4: Wrap dualWriteToSourceCheck in transaction + await (epic #4017) #4039 — C4: Wrap dualWriteToSourceCheck in transaction + await (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
C5: Surface claim-linking failures in 5 batch sync responses (epic #4017) #4040 — C5: Surface claim-linking failures in 5 batch sync responses (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
B3 (narrowed): Unique index on policy_stakeholders natural key (epic #4017) #4041 — B3 (narrowed): Unique index on policy_stakeholders natural key (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
D1+D2+D5: Incident dedup race, ID allocation, deterministic YAML load (epic #4017) #4043 — D1+D2+D5: Incident dedup race, ID allocation, deterministic YAML load (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
D4: Build pipeline fail-closed — abort on YAML errors, surface wiki-server failures (epic #4017) #4044 — D4: Build pipeline fail-closed — abort on YAML errors, surface wiki-server failures (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
D3: Fix entity slug reassignment race — bulk UPDATE replaces per-slug loop (epic #4017) #4045 — D3: Fix entity slug reassignment race — bulk UPDATE replaces per-slug loop (epic Epic: Data integrity: systemic gaps across schema, validation, write paths #4017)
All reactions