feat(ai-partner): corpus gap capture — D1 + detection + GitHub sync (#1471)#1482
Merged
Merged
Conversation
New top-level ai-proxy/ directory with the Worker that sits between the
mobile client and Anthropic/OpenAI. Co-located with the rest of the repo
so the client and proxy evolve together.
Endpoints:
- GET /ai/health — liveness; returns {status, version}
- POST /ai/embed — OpenAI text-embedding-3-small, 1536-dim vector
- POST /ai/chat — streaming Anthropic SSE with gap-signal detection
Auth: RevenueCat receipt validated with 5-min KV cache keyed on the
SHA-256 of the receipt; 401 / 402 / 503 classified by failure kind.
Rate limit: per-user monthly + 10-minute burst counters in KV.
premium: 300/month + 10/burst
partner_plus: 1,500/month + 30/burst
Anthropic client: zero-retention header, server-side system prompt
assembly, partner_plus always routes to Sonnet regardless of client
hint.
Gap detection: parses the trailing {"gap": ...} JSON envelope from the
streamed response; writes a stub D1 row + structured log today. #1471
replaces the stub with full D1 schema + semantic dedup + GitHub sync.
Logging: metadata-only — user_hash, endpoint, status, latency,
entitlement. Never logs request bodies, response text, retrieved
chunks, or profile summaries.
Tests: 37 Vitest tests across auth, rate-limit, gap-detection, and
Anthropic client. All run offline via fetch interception + in-memory
KV stub.
https://claude.ai/code/session_01Pht3kzgdvkn81DDfL9SnFe
…1471) Full implementation of the corpus-gap feedback flywheel (stacked on #1450). Proxy (ai-proxy/): - src/gapDetection.ts — rewritten from stub to full pipeline: · parseGapSignal() — trailing envelope parser (unchanged interface) · isLowRetrievalScore() — triggers gap when max score < 0.55 · scrubPII() — light regex scrub (email/phone/url/card) · cosineSimilarity / packEmbedding / unpackEmbedding · findSemanticMatch() — cosine ≥ 0.9 → increment occurrence_count · generateScrubbedSummary() — Haiku paraphrase for GitHub issue body · captureGap() — full D1 INSERT (or UPDATE on dedup match) · redactGap() — hard-nuke endpoint - src/index.ts — chat handler now combines model-signal OR low-score into an effective gap; embeds the question; passes retrieval chunk ids + profile + chapter ref into captureGap. New endpoints: · POST /ai/feedback — thumbs-down capture · DELETE /ai/gaps/:id — admin redact (partner_plus entitlement) - migrations/0001_corpus_gaps.sql — D1 schema with corpus_gaps table, amicus_config table seeded with gap_sync_mode='individual' - README.md — documents endpoints, D1 apply, config flag, sync script _tools/corpus_gap_sync.py: - Scheduled runner that pulls status='new' D1 rows, creates GitHub issues labeled corpus-gap (Partner Gaps swim lane), marks rows issue_opened. - Two modes: individual (one issue per gap) or digest (singletons roll up daily, clusters ≥3 still get their own). Switched by the D1 config row — no backend change at the 20K-user trigger. - Dry-run mode for verification. Tests: proxy suite now 49 passing (+12 gap-detection tests covering parse, scrub, low-score detection, cosine, pack/unpack, constants). https://claude.ai/code/session_01Pht3kzgdvkn81DDfL9SnFe
Content Pipeline Results✅ All pipeline checks passed
|
Test Results✅ All tests passed
Coverage
⏱️ Duration: 72.0s |
CodeQL flagged unbounded repetition in EMAIL_RE. Tighten all PII scrub regexes with realistic upper bounds (64-char local part, 253-char domain, 63-char TLD per RFC/IANA, 20-char phone body, 2048-char URL, 19-digit card) so pathological input can't cause polynomial backtracking. https://claude.ai/code/session_01Pht3kzgdvkn81DDfL9SnFe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1471. Depends on #1450 (stacked).
Summary
Full corpus-gap feedback flywheel — every "I don't have that in my corpus" becomes a content-roadmap signal routed into the Partner Gaps swim lane.
Gap triggers (any one captures)
{"gap": true, "gap_type": ..., "topic": ...}envelope on the chat stream.GAP_SIMILARITY_FLOOR).POST /ai/feedbackendpoint.Proxy changes (
ai-proxy/)src/gapDetection.ts— full pipeline: parse envelope, PII scrub, cosine dedup vs. recent open gaps (threshold 0.9), Haiku-paraphrased scrubbed summary, D1 INSERT or UPDATE.src/index.ts— chat handler tees stream, embeds the question once, combines signals, and passes rich context intocaptureGap. New endpoints:POST /ai/feedback— thumbs-down captureDELETE /ai/gaps/:id— hard redact (admin-gated: partner_plus + Cloudflare Access)migrations/0001_corpus_gaps.sql— D1 schema forcorpus_gaps+amicus_config(seeded withgap_sync_mode=individual)README.md— documents new endpoints, D1 apply, sync script, config flagGitHub sync (
_tools/corpus_gap_sync.py)Scheduled runner that pulls
status='new'gaps from D1 and creates GitHub issues labeledcorpus-gap(Partner Gaps swim lane), then marks rowsissue_opened. Two modes:amicus_config.gap_sync_moderow — pure config change, no schema migration.Supports
--dry-runfor verification.Test plan
cd ai-proxy && npx tsc --noEmitcleancd ai-proxy && npm test— 49 tests pass (+12 new gap-detection tests: parse, scrub, low-score detection, cosine, pack/unpack, constants)python3 -c "import ast; ast.parse(open('_tools/corpus_gap_sync.py').read())"— syntax cleanisLowRetrievalScorefires, verify row lands in D1. Then runpython3 _tools/corpus_gap_sync.py --dry-runwith D1/GitHub tokens and confirm issue titles look right.Privacy posture
question_textis lightly scrubbed (email/phone/URL/card) and stored in D1 for Craig's reference only.scrubbed_summary(Haiku-paraphrased, non-identifying).DELETE /ai/gaps/:idwipes the row's question text, embedding, summary, profile, and chunk json.Out of scope
https://claude.ai/code/session_01Pht3kzgdvkn81DDfL9SnFe