Skip to content

feat(targeting): extract fcap into Service + Valkey 9 + glide#108

Open
ohalushchak-exadel wants to merge 3 commits intomainfrom
ohalushchak-exadel/fcap-service
Open

feat(targeting): extract fcap into Service + Valkey 9 + glide#108
ohalushchak-exadel wants to merge 3 commits intomainfrom
ohalushchak-exadel/fcap-service

Conversation

@ohalushchak-exadel
Copy link
Copy Markdown
Collaborator

Summary

  • New targeting/fcap package: Service over a Store interface for frequency-cap state. Keys are fcap:{sha256(user_identity)[:16]}, fields are literal {seller_agent_url}:{package_id} strings, value "1", TTL set via HSETEX to the end of the cap window. Writes happen on the last allowed exposure (caller decides).
  • New targeting/glidestore package: single valkey-glide/go/v2 client implementing both targeting.Store (engine) and fcap.Store. Replaces the deleted targeting/valkeystore (go-redis) submodule.
  • Engine is now segment-only. Removed: RecordExposure, campaign-level caps (CampaignFreqConfig + helpers), intent score (ComputeIntentScore, LatestExposure*), ExposureLog/ExposureEntry and the binary-log path, sorted-set helpers (ZAdd/ZCount/ZExpire/ZRemRangeByScore), FrequencyRule primitive, and IntentScore from tmproto.PackageEligibility and bench types/fixtures.
  • Reference identity-agent: switched from valkeystore/go-redis to glidestore/valkey-glide. fcap.Service is initialized but not wired to an HTTP route — the exposure-write surface is intentionally out of scope (no caller today).
  • go fix ./... rewrites picked up along the way (maps.Copy, sync.WaitGroup.Go, gofmt realignment in targeting/entity.go, e2e/e2e_test.go, router/router.go, router/providerset_test.go).

Schema

Old New
Key user:exposures:{tokenHash} (binary blob, 30d TTL) fcap:{tokenHash} (HSET, per-field TTL)
Value append-only binary log (40-byte entries) {url}:{pkg} → "1", TTL = end of window
Read scan log + count vs. MaxCount HEXISTS
Write read-modify-write whole blob (non-atomic) HSETEX (preferred)
Campaign caps yes (sorted set + counts) dropped
Intent score yes (linear decay over 7d) dropped

Test plan

  • go build ./... clean across all 4 modules (root, e2e, bench, reference/identity-agent)
  • go test ./... clean across all 4 modules — segment/property/URL/topic engine logic still covered by unit tests against targeting.MockStore
  • fcap.Service unit tests against fcap.MockStore cover plumbing (hashing, field formatting, batch fan-out)
  • go test -tags integration ./targeting/glidestore/... against a real Valkey 9 container (testcontainers): 11 tests, ~13s end-to-end
    • fcap.Store: HSETEX write + HEXISTS read + TTL expiry + batch read/write
    • targeting.Store: Get/Set/MGet/MSet/SetIsMember/SetIntersect/SetMembers/Exists across types + TTL
    • Engine end-to-end (EvaluateContext + EvaluateIdentityResolved) reading topic/URL/segment data from real Valkey
  • go vet ./... clean across all 4 modules
  • go fix ./... clean across all 4 modules

Open follow-ups (out of scope here)

  • Reference identity-agent has no HTTP endpoint to record caps. Whoever owns exposure recording will wire a route or call fcap.Service.RecordCap directly.
  • No counting/window logic ships with this change. The engine no longer tracks impression counts; whichever component decides "this is the Nth exposure" calls RecordCap on the last allowed one.

🤖 Generated with Claude Code

…y Valkey 9 + glide

Replaces the engine's inline frequency-cap logic (binary exposure log + sorted
sets) with a dedicated fcap package. State now lives as Valkey hash fields
keyed by hashed user identity (fcap:{hash}), one field per (seller_agent_url,
package_id) tuple, with TTL set to the end of the cap window via HSETEX. Caller
decides when the cap fires (writes happen on the last allowed exposure).

Drops: campaign-level caps, intent score, ExposureLog/Entry/binary log path,
RecordExposure, and the go-redis-backed valkeystore module. Switches the
Valkey client to valkey-glide/go/v2; bumps the runtime to Valkey 9.

The engine is now segment-only; callers compose engine + fcap.Service.

Adds testcontainers-based Valkey 9 integration tests for both the
fcap.Store and targeting.Store interfaces, plus end-to-end engine flows
against real Valkey, behind the integration build tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

IPR Policy Agreement Required

@ohalushchak-exadel — thanks for the contribution. Before this PR can be merged, the AgenticAdvertising.Org IPR Policy requires your agreement.

To agree, post a new comment on this PR with the exact phrase:

I have read the IPR Policy

Your signature is recorded once and covers all contributions to AAO repositories. See signatures/README.md for what gets recorded and why.

ohalushchak-exadel and others added 2 commits April 30, 2026 22:10
- Drop dead Engine.Now/now() and loadPackageIdentityConfig (no callers).
- Tighten coerceBool: HEXISTS batch result is plain bool; surface %T on mismatch.
- Defend MGet length in glidestore for symmetry with FieldExistsBatch.
- Annotate Store.MSet TTL atomicity at the interface level.
- Reference agent: drop unused fcap.Service construction, simplify initStore
  to a single targeting.Store, comment the in-memory fallback's intent, and
  return errors from seedConfigs instead of os.Exit(1).
- Pin fcap field-name format with fieldDelimiter constant + format-pinned test.
- Drop misleading "matches HashToken" coupling comment on identityKey.
- Rename targeting/exposure.go -> targeting/identity.go (the file no longer
  contains exposure types).
- Update UserProfile doc to drop "intent score" reference.
- Add fcap.MockStore TTL unit test (mock-only path was uncovered).
- Document segment-default-eligible semantics on EvaluateIdentityResolved.
- Add multi-identity segment-union engine test (regression cover for
  MergeUserProfiles fan-out across identities).
- Trim Redis from targeting.Store doc; bump fcap.Store doc to "Valkey 9+".
- gofmt fcap integration test struct-literal alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modernizations from `go fix ./...`:
- C-style index loops -> range over int
- Manual slice-contains loops -> slices.Contains

Module versions normalized by `go mod tidy` (go 1.25 -> go 1.25.0).
go.sum entries refreshed where indirect deps shifted.

No behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ohalushchak-exadel
Copy link
Copy Markdown
Collaborator Author

I have read the IPR Policy

@bokelley
Copy link
Copy Markdown
Contributor

Thanks for confirming, @ohalushchak-exadel — noted.


Generated by Claude Code

@ohalushchak-exadel ohalushchak-exadel marked this pull request as ready for review April 30, 2026 20:18
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