Skip to content

feat(org-lens): extend shared Valkey cache across Org Lens SSR surface (LFXV2-2356)#970

Merged
luismoriguerra merged 6 commits into
mainfrom
feat/LFXV2-2356
Jun 16, 2026
Merged

feat(org-lens): extend shared Valkey cache across Org Lens SSR surface (LFXV2-2356)#970
luismoriguerra merged 6 commits into
mainfrom
feat/LFXV2-2356

Conversation

@luismoriguerra

Copy link
Copy Markdown
Contributor

Summary

Extends the existing shared Valkey cache (the spec-026 CachePort/ValkeyService foundation) across the entire Org Lens server read surface that was previously uncached, using two keying models:

  • Per-org keys for caller-invariant Snowflake reads (WHERE ACCOUNT_ID = ?), refreshed on the daily dbt batch (1h TTL): Overview foundations + the 6 involvement metrics + account-context, Memberships (active/expired/discover), Training, Contributions, Events, and the People Snowflake tabs (All Employees, Contributors, Trainees, Event Attendees).
  • Per-user keys for FGA/token-filtered reads (30s TTL): the org-wide committee seat drain (now fetched once and shared across the Board tab, Committee tab, and directory pickers), Key Contacts, and the Org Lens Access list.

Also migrates the org people directory off its per-instance in-memory Map onto the shared cache, finishing the cross-instance-coherence migration started in spec 026.

Design / safety

  • Fail-soft: cache faults/timeouts degrade to a direct fetch (≤250ms op bound), pages never error on cache outage.
  • Fail-closed: unresolved/unsafe identity → null key → direct fetch; no cross-caller sharing of permissioned data.
  • Per-request authz stays in front of the cache; only already-authorized results are stored.
  • Shape-guards reject corrupt/legacy entries (degrade to a miss); only successful results are cached; the access write-refresh bypass is preserved.
  • Account-context fan-out is bounded by a worker pool to protect the Snowflake connection pool on a cold cache.

Test plan

  • yarn lint:check, yarn check-types (forced tsc on app + shared), yarn format:check — all pass
  • Pre-PR ui-code-standard-enforcer + ui-code-review passes (fixes applied)
  • Manual verification against dev (dev-ui-only) with a local Valkey: all Org Lens pages render; 15 cache keys written with correct per-org / per-user namespaces + 1h TTL; cache HIT confirmed via TTL decrement on reload
  • Fail-soft confirmed: with VALKEY_URL unset, all pages render via direct fetch
  • Validate cross-instance coherence on dev/staging deploy (where VALKEY_URL points at ElastiCache)

Made with Cursor

LFXV2-2356)

Put a read-through cache in front of every uncached Org Lens server read,
reusing the existing CachePort/ValkeyService foundation, with two keying models:

- Per-org keys for caller-invariant Snowflake reads (WHERE ACCOUNT_ID = ?),
  refreshed on the daily dbt batch (1h TTL): overview foundations + involvement,
  memberships, training, contributions, events, and the People Snowflake tabs.
- Per-user keys for FGA/token-filtered reads (30s TTL): the org-wide committee
  seat drain (now shared once across the Board, Committee, and directory pickers),
  key contacts, and the org-lens access list.

Also migrates the org people directory off its per-instance in-memory Map onto
the shared cache, finishing the cross-instance-coherence migration. Caching stays
fail-soft (faults degrade to a direct fetch) and fail-closed (unresolved identity
bypasses the cache), with shape guards rejecting corrupt entries and successful
results only. The account-context fan-out is bounded by a worker pool to protect
the Snowflake connection pool on a cold cache.

Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 16, 2026 19:00
@luismoriguerra luismoriguerra requested a review from a team as a code owner June 16, 2026 19:00
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Valkey-backed caching to all Org Lens Snowflake-reading services. New withOrgCache and withPerUserCache wrappers are introduced in valkey.service.ts with fail-closed key builders. Each service is refactored to split Snowflake queries into private raw-fetch helpers, add runtime type guards for cache validation, and assemble mapped responses from cached raw rows. Per-account caching for org-context fetches is implemented via a concurrency-limited worker pool.

Changes

Valkey Org Lens caching rollout

Layer / File(s) Summary
Cache infrastructure: constants, key builders, and read-through wrappers
packages/shared/src/constants/valkey-cache.constants.ts, packages/shared/src/constants/org-selector.constants.ts, packages/shared/src/interfaces/valkey-cache.interface.ts, apps/lfx-one/src/server/services/valkey.service.ts
VALKEY_CACHE gains org-lens namespaces (ORG_LENS_SNOWFLAKE_NAMESPACE, ORG_SEATS_NAMESPACE, ORG_PEOPLE_KC_NAMESPACE, ORG_ACCESS_LIST_NAMESPACE, ORG_PEOPLE_DIRECTORY_NAMESPACE) and TTL values (3600s Snowflake, 30s per-user). ORG_LENS_ACCOUNT_CONTEXT_FETCH_CONCURRENCY=8 is added. valkey.service.ts exports buildOrgCacheKey, buildPerUserOrgKey, withOrgCache, withPerUserCache, invalidatePerUserCache, and adds del(key) method with fail-closed behavior on null or unsafe inputs. CachePort interface gains del method.
Simple org-scoped caching
apps/lfx-one/src/server/services/org-contributions.service.ts, apps/lfx-one/src/server/services/org-involvement.service.ts, apps/lfx-one/src/server/services/org-lens-foundations.service.ts, apps/lfx-one/src/server/services/org-lens-memberships.service.ts, apps/lfx-one/src/server/services/org-people-contributors.service.ts, apps/lfx-one/src/server/services/org-people-event-attendees.service.ts, apps/lfx-one/src/server/services/org-people-trainees.service.ts
Each service wraps its public query method with withOrgCache, extracts the Snowflake query into a private raw-fetch helper, adds a runtime type guard (isContributionsRaw, hasAccountId, isObjectRowArray, isRawRowArray, isEventAttendeesRaw, isTraineesRaw), and reassembles the mapped response from cached raw rows. Deterministic cache-key suffixes are added where query parameters affect SQL.
Events service org-scoped caching with paramSignature and row validators
apps/lfx-one/src/server/services/org-lens-events.service.ts
Applies withOrgCache to getOrgEvents (list + pagination), getOrgEventsSummary (fixed key), getEventAttendees (drawer), and getEventSpeakers (drawer). Introduces paramSignature() helper for deterministic cache-key suffixes based on query parameters. Adds runtime row validators (isObjectRow, isObjectRowArray, isEventRowArray, isContactRowArray) to guard cached shapes. Each endpoint fetches rows via private helpers and derives total/metadata from cached rows.
Complex bundled-payload caching
apps/lfx-one/src/server/services/org-lens-people.service.ts, apps/lfx-one/src/server/services/org-lens-training.service.ts
OrgLensPeopleService introduces raw row type for ENGAGED_FOUNDATION_IDS, bundles three concurrent reads via fetchAllEmployeesRaw, adds fetchEmployeeDetailRaw with filter-safe bypass and runEmployeeDetailFetch orchestrator, validates shapes via isAllEmployeesRaw/isEmployeeDetailRaw. OrgLensTrainingService adds fetchTrainingStatsRows, changes fetchPagedCourseRows to return {rows, total}, splits roster into fetchRosterRows+mapRosterRows, adds paramSignature helper, validates via isTrainingStatsRaw/isPagedCourseRaw/isRosterRowArray.
Per-user caching: access list, board seats, people directory, key contacts
apps/lfx-one/src/server/services/org-lens-access.service.ts, apps/lfx-one/src/server/services/org-lens-board-committee.service.ts, apps/lfx-one/src/server/services/org-people-directory.service.ts, apps/lfx-one/src/server/services/org-people-key-contacts.service.ts
Each service uses withPerUserCache keyed by effective username. listAccessUsers bypasses cache when knownCanManage is provided and adds computeAccessList helper; getAccessPrincipals uses separate :principals namespace. Write operations invalidate caller's cached entries before returning refreshed list. OrgPeopleDirectoryService removes in-memory TTL/Map eviction logic entirely. Type guards isAccessListResponse, isAllEmployeesResponse, isKeyContactsResponse, isOrgSeatArray validate cached payloads.
Per-account org-context cache with concurrency-limited worker pool
apps/lfx-one/src/server/services/organization.service.ts
getOrgLensAccountContext is rewritten from a single bulk IN (...) Snowflake query to per-account withOrgCache calls executed through a concurrency-limited worker pool (limit ORG_LENS_ACCOUNT_CONTEXT_FETCH_CONCURRENCY=8). Results are deduplicated, individually cached, flattened, and sorted by accountName. fetchOrgLensAccountContext issues a single-account WHERE ACCOUNT_ID = ? query. isAccountContextArray guards cached values.

Sequence Diagram(s)

sequenceDiagram
  participant Service as Org Lens Service
  participant withOrgCache
  participant buildOrgCacheKey
  participant ValkeyService as valkeyService
  participant TypeGuard as accept(value)
  participant SnowflakeSvc as Snowflake

  Service->>withOrgCache: withOrgCache(accountId, subResource, ttl, fetcher, accept)
  withOrgCache->>buildOrgCacheKey: buildOrgCacheKey(accountId, subResource)
  alt accountId filter-safe
    buildOrgCacheKey-->>withOrgCache: key
  else accountId unsafe
    buildOrgCacheKey-->>withOrgCache: null
    withOrgCache->>SnowflakeSvc: fetcher() [bypass cache]
  end
  alt valid key
    withOrgCache->>ValkeyService: withCache(key, ttl, fetcher, accept)
    alt cache hit
      ValkeyService->>TypeGuard: accept(cachedValue)
      alt accept=true
        TypeGuard-->>ValkeyService: true
        ValkeyService-->>withOrgCache: cached raw rows
      else accept=false
        TypeGuard-->>ValkeyService: false [corrupt/legacy]
        ValkeyService->>SnowflakeSvc: fetcher()
        SnowflakeSvc-->>ValkeyService: raw rows
        ValkeyService-->>withOrgCache: raw rows [store in cache]
      end
    else cache miss
      ValkeyService->>SnowflakeSvc: fetcher()
      SnowflakeSvc-->>ValkeyService: raw rows
      ValkeyService-->>withOrgCache: raw rows [store in cache]
    end
  end
  withOrgCache-->>Service: raw rows
  Service->>Service: map raw rows → typed response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • linuxfoundation/lfx-self-serve#943: Modifies OrgContributionsService.getContributions Snowflake fetch/mapping flow—this PR wraps that same method with Valkey caching and raw-fetch helpers.
  • linuxfoundation/lfx-self-serve#844: Introduces OrgLensTrainingService training stats query logic that this PR wraps with withOrgCache and raw-fetch/type-guard helpers.
  • linuxfoundation/lfx-self-serve#914: Modifies valkey.service.ts cache infrastructure that this PR extends with withOrgCache/withPerUserCache and new fail-closed key builders.

Suggested labels

enhancement, org-lens, ai-assisted

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(org-lens): extend shared Valkey cache across Org Lens SSR surface (LFXV2-2356)' accurately describes the primary change: extending Valkey caching across the Org Lens server-side read surface with specific focus on the feature aspect.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the two keying models (per-org and per-user), safety design patterns, testing completed, and iterative review improvements across four rounds, all of which align with the file-level changes shown in the raw summary.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXV2-2356

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI 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.

Pull request overview

This PR extends the existing shared Valkey (Redis) read-through cache across the Org Lens SSR/server read surface, adding standardized key namespaces/TTLs and wrapping previously-uncached Snowflake and permission-scoped reads with withOrgCache / withPerUserCache helpers.

Changes:

  • Added Org Lens–specific cache namespaces and TTL constants to the shared VALKEY_CACHE configuration.
  • Introduced server helpers for consistent per-org and per-user cache-key construction and read-through caching.
  • Applied caching across Org Lens services (Snowflake-backed reads and per-user permission-filtered reads), including migrating the people directory off in-memory caching.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/shared/src/constants/valkey-cache.constants.ts Adds Org Lens cache namespaces and TTL constants for per-org/per-user caching.
packages/shared/src/constants/org-selector.constants.ts Adds a concurrency cap constant for Org Lens account-context cache warmup.
apps/lfx-one/src/server/services/valkey.service.ts Adds key-prefix/key-builder helpers and withOrgCache/withPerUserCache wrappers over ValkeyService.withCache.
apps/lfx-one/src/server/services/organization.service.ts Caches Org Lens account-context reads per accountId with a bounded concurrency worker pool.
apps/lfx-one/src/server/services/org-people-trainees.service.ts Wraps bundled trainees tab Snowflake reads in per-org cache with a shape guard.
apps/lfx-one/src/server/services/org-people-key-contacts.service.ts Adds per-user caching for key contacts using effective username + org UID keying.
apps/lfx-one/src/server/services/org-people-event-attendees.service.ts Wraps bundled event attendees tab Snowflake reads in per-org cache with a shape guard.
apps/lfx-one/src/server/services/org-people-directory.service.ts Migrates the unified directory from in-memory Map caching to shared per-user Valkey caching.
apps/lfx-one/src/server/services/org-people-contributors.service.ts Caches contributors tab Snowflake reads per-org with a time-range subresource suffix.
apps/lfx-one/src/server/services/org-lens-training.service.ts Adds per-org caching for training stats, paged lists, and roster drawers with param signatures.
apps/lfx-one/src/server/services/org-lens-people.service.ts Caches the “All Employees” bundle and per-person detail bundle per org; moves mapping after cache read.
apps/lfx-one/src/server/services/org-lens-memberships.service.ts Caches raw Snowflake membership rows per org and keeps filtering out of cache key.
apps/lfx-one/src/server/services/org-lens-foundations.service.ts Caches foundations/projects response per org with a response shape guard.
apps/lfx-one/src/server/services/org-lens-events.service.ts Adds per-org caching for event list/summary and drawer reads using a param signature.
apps/lfx-one/src/server/services/org-lens-board-committee.service.ts Adds per-user caching for the org-wide seat drain used by multiple Org Lens consumers.
apps/lfx-one/src/server/services/org-lens-access.service.ts Adds per-user caching for access list and principals reads; preserves bypass for post-write refresh.
apps/lfx-one/src/server/services/org-involvement.service.ts Adds per-org caching across involvement metrics endpoints with a generic accountId guard.
apps/lfx-one/src/server/services/org-contributions.service.ts Wraps contributions page Snowflake fan-out (KPIs, lists, options) in per-org caching keyed by query signature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/lfx-one/src/server/services/valkey.service.ts Outdated
Comment thread apps/lfx-one/src/server/services/org-lens-access.service.ts
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

🚀 Deployment Status

Your branch has been deployed to: https://ui-pr-970.dev.v2.cluster.linuxfound.info

Deployment Details:

  • Environment: Development
  • Namespace: ui-pr-970
  • ArgoCD App: ui-pr-970

The deployment will be automatically removed when this PR is closed.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/lfx-one/src/server/services/org-lens-access.service.ts (1)

82-91: 💤 Low value

Consider a stricter shape guard for OrgAccessUser[].

Using Array.isArray as the validator accepts any array shape. If a corrupt/legacy entry stores [1, 2, 3] instead of OrgAccessUser[], it would pass validation and potentially cause runtime issues when consumers access .email, .role, etc.

A minimal element check (e.g., verifying the first element has expected fields) would provide stronger protection while remaining lightweight.

♻️ Optional: stricter validator
-      Array.isArray
+      (v: unknown) => Array.isArray(v) && (v.length === 0 || (typeof (v as OrgAccessUser[])[0]?.email === 'string'))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/org-lens-access.service.ts` around lines 82
- 91, The getAccessPrincipals method uses Array.isArray as the shape guard for
the withPerUserCache call, which is too permissive and would accept any array
shape including corrupt data like [1, 2, 3]. Replace the Array.isArray validator
with a stricter shape guard that checks both that the cached value is an array
AND that its elements conform to the OrgAccessUser structure (e.g., by verifying
the first element has expected fields like email and role). This prevents
runtime errors when downstream code tries to access properties on the cached
principals.
apps/lfx-one/src/server/services/org-contributions.service.ts (1)

459-475: ⚡ Quick win

Canonicalize cache-signature inputs to improve hit rate.

Line 460 currently keys on raw query.search and raw arrays. Since SQL uses trimmed search and set-like IN semantics, normalize (trim/case-normalize, de-dup) before signature encoding to avoid avoidable key fragmentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/org-contributions.service.ts` around lines
459 - 475, The contributionsSignature function uses raw query values which can
fragment the cache key when logically identical queries differ only in
whitespace or array ordering. Modify the function to normalize inputs before
signature encoding: trim and case-normalize the query.search field, and
deduplicate the query.projects and query.employees arrays (by converting to Set
and back to array) before sorting and joining them. This will ensure that
semantically identical SQL queries generate the same cache key regardless of
input formatting variations.
apps/lfx-one/src/server/services/org-lens-training.service.ts (1)

491-516: 💤 Low value

Consider extracting paramSignature and isObjectRowArray to shared utilities.

These helpers are duplicated across org-lens-events.service.ts, org-people-contributors.service.ts, and org-lens-training.service.ts. While the functions are small and the duplication is harmless, extracting them to valkey.service.ts (alongside withOrgCache) would consolidate the caching utilities in one place.

This is minor and can be deferred if preferred.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/org-lens-training.service.ts` around lines
491 - 516, The functions paramSignature and isRosterRowArray are duplicated
across multiple service files. Extract these utility functions to
valkey.service.ts alongside other caching utilities like withOrgCache, then
remove the duplicate definitions from each service file and import them from
valkey.service.ts instead. This consolidates related utilities in a single
location for better maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/lfx-one/src/server/services/org-contributions.service.ts`:
- Around line 477-487: The isContributionsRaw function currently only validates
that each field is an array but does not check the structure of the row objects
within those arrays. Corrupt or legacy cached row objects can pass this
validation and cause errors downstream in mapRepoRow, mapCommitRow, and option
mappers. Add validation logic to check representative row elements from each
array (kpiRows, repoRows, commitRows, projectOptionRows, employeeOptionRows) by
sampling at least one element from each non-empty array and verifying that
required properties exist on those representative row objects before the
function returns true.

In `@apps/lfx-one/src/server/services/organization.service.ts`:
- Around line 927-931: The type guard isAccountContextArray validates only
accountId as a string, but the sort operation at line 923 calls localeCompare on
accountName which requires it to be a string. Enhance the guard in
isAccountContextArray to also validate that accountName is a string property on
each element, ensuring corrupt or legacy cache entries that lack this property
cannot pass the guard and subsequently crash during the sort operation.
- Around line 897-924: The getOrgLensAccountContext method now processes each
account ID individually through the worker pool, which means duplicate IDs in
the input array will produce duplicate rows in the response, whereas the prior
implementation collapsed duplicates naturally through a single IN query.
Deduplicate the accountIds array upfront before building the worker queue to
preserve the prior behavior. This should be done immediately after the initial
length check (after the early return for empty array) by converting the array to
a Set and back to an array, or using another deduplication approach, ensuring
the poolSize and cursor-based iteration operate on the deduplicated list.

---

Nitpick comments:
In `@apps/lfx-one/src/server/services/org-contributions.service.ts`:
- Around line 459-475: The contributionsSignature function uses raw query values
which can fragment the cache key when logically identical queries differ only in
whitespace or array ordering. Modify the function to normalize inputs before
signature encoding: trim and case-normalize the query.search field, and
deduplicate the query.projects and query.employees arrays (by converting to Set
and back to array) before sorting and joining them. This will ensure that
semantically identical SQL queries generate the same cache key regardless of
input formatting variations.

In `@apps/lfx-one/src/server/services/org-lens-access.service.ts`:
- Around line 82-91: The getAccessPrincipals method uses Array.isArray as the
shape guard for the withPerUserCache call, which is too permissive and would
accept any array shape including corrupt data like [1, 2, 3]. Replace the
Array.isArray validator with a stricter shape guard that checks both that the
cached value is an array AND that its elements conform to the OrgAccessUser
structure (e.g., by verifying the first element has expected fields like email
and role). This prevents runtime errors when downstream code tries to access
properties on the cached principals.

In `@apps/lfx-one/src/server/services/org-lens-training.service.ts`:
- Around line 491-516: The functions paramSignature and isRosterRowArray are
duplicated across multiple service files. Extract these utility functions to
valkey.service.ts alongside other caching utilities like withOrgCache, then
remove the duplicate definitions from each service file and import them from
valkey.service.ts instead. This consolidates related utilities in a single
location for better maintainability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1b82ca0-747a-4976-bce0-5f536d940f00

📥 Commits

Reviewing files that changed from the base of the PR and between 722a4d5 and e1a96ee.

📒 Files selected for processing (18)
  • apps/lfx-one/src/server/services/org-contributions.service.ts
  • apps/lfx-one/src/server/services/org-involvement.service.ts
  • apps/lfx-one/src/server/services/org-lens-access.service.ts
  • apps/lfx-one/src/server/services/org-lens-board-committee.service.ts
  • apps/lfx-one/src/server/services/org-lens-events.service.ts
  • apps/lfx-one/src/server/services/org-lens-foundations.service.ts
  • apps/lfx-one/src/server/services/org-lens-memberships.service.ts
  • apps/lfx-one/src/server/services/org-lens-people.service.ts
  • apps/lfx-one/src/server/services/org-lens-training.service.ts
  • apps/lfx-one/src/server/services/org-people-contributors.service.ts
  • apps/lfx-one/src/server/services/org-people-directory.service.ts
  • apps/lfx-one/src/server/services/org-people-event-attendees.service.ts
  • apps/lfx-one/src/server/services/org-people-key-contacts.service.ts
  • apps/lfx-one/src/server/services/org-people-trainees.service.ts
  • apps/lfx-one/src/server/services/organization.service.ts
  • apps/lfx-one/src/server/services/valkey.service.ts
  • packages/shared/src/constants/org-selector.constants.ts
  • packages/shared/src/constants/valkey-cache.constants.ts

Comment thread apps/lfx-one/src/server/services/org-contributions.service.ts
Comment thread apps/lfx-one/src/server/services/organization.service.ts
Comment thread apps/lfx-one/src/server/services/organization.service.ts
Address review feedback on the Org Lens cache rollout:
- buildPerUserOrgKey now fails closed when the org uid isn't filter-safe,
  so it can't corrupt the ':'-delimited key (matches buildOrgCacheKey).
- getAccessPrincipals and fetchAllOrgSeats accept guards require non-null
  object elements instead of bare Array.isArray, so corrupt entries miss.
- isContributionsRaw samples a representative row per array (non-null object
  carrying its contract key) before accepting a cache hit.
- getOrgLensAccountContext dedupes account ids before fan-out to preserve the
  prior IN(...) collapse, and isAccountContextArray also validates accountName
  to protect the localeCompare sort.

Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>
@luismoriguerra

Copy link
Copy Markdown
Contributor Author

Review round recap — cache hardening (commit a44e55f)

Addressed all 6 open automated-review threads (Copilot + CodeRabbit), all in the original cache scope. No out-of-scope feedback arrived.

# File Fix
1 valkey.service.ts buildPerUserOrgKey fails closed when orgUid isn't filter-safe (symmetric with buildOrgCacheKey)
2 org-lens-access.service.ts getAccessPrincipals accept guard requires non-null object elements (isPrincipalArray)
3 org-lens-board-committee.service.ts fetchAllOrgSeats accept guard requires non-null object elements (isOrgSeatArray)
4 org-contributions.service.ts isContributionsRaw samples a representative row per array carrying its contract key
5 organization.service.ts getOrgLensAccountContext dedupes accountIds before fan-out
6 organization.service.ts isAccountContextArray also validates accountName (protects localeCompare sort)

Cache invariants preserved: fail-soft/fail-closed, per-org vs per-user keying, successful-results-only writes, TTLs, the access write-refresh bypass, and the bounded account-context worker pool. Public method signatures unchanged.

Quality gate: yarn format:check ✅ · yarn lint:check ✅ · yarn build (incl. tsc) ✅

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/lfx-one/src/server/services/org-lens-access.service.ts (1)

27-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate principal shape, not just object-ness.

Line 36 still accepts {} as an OrgAccessUser, so a malformed cached principals entry can be treated as a hit and break later field reads. Reuse the same nested guard for isAccessListResponse.users as well.

Proposed guard tightening
 function isAccessListResponse(value: unknown): boolean {
   const v = value as Partial<OrgAccessListResponse> | null;
   return (
-    !!v && typeof v.orgUid === 'string' && Array.isArray(v.users) && typeof v.summary === 'object' && v.summary !== null && typeof v.canManage === 'boolean'
+    !!v &&
+    typeof v.orgUid === 'string' &&
+    isPrincipalArray(v.users) &&
+    isAccessSummary(v.summary) &&
+    typeof v.canManage === 'boolean'
   );
 }
 
 /** Rejects a corrupt/legacy principals entry whose elements aren't non-null objects (degrades to a miss before the directory merge reads user fields). */
 function isPrincipalArray(value: unknown): boolean {
-  return Array.isArray(value) && value.every((el) => el !== null && typeof el === 'object' && !Array.isArray(el));
+  return Array.isArray(value) && value.every(isOrgAccessUser);
+}
+
+function isOrgAccessUser(value: unknown): value is OrgAccessUser {
+  const user = value as Partial<OrgAccessUser> | null;
+  return (
+    !!user &&
+    typeof user === 'object' &&
+    !Array.isArray(user) &&
+    typeof user.email === 'string' &&
+    typeof user.name === 'string' &&
+    typeof user.initials === 'string' &&
+    (user.avatarUrl === null || typeof user.avatarUrl === 'string') &&
+    (user.jobTitle === null || typeof user.jobTitle === 'string') &&
+    (user.role === 'admin' || user.role === 'viewer') &&
+    typeof user.inviteStatus === 'string' &&
+    typeof user.isPending === 'boolean'
+  );
+}
+
+function isAccessSummary(value: unknown): value is OrgAccessSummary {
+  const summary = value as Partial<OrgAccessSummary> | null;
+  return (
+    !!summary &&
+    typeof summary === 'object' &&
+    !Array.isArray(summary) &&
+    typeof summary.totalUsers === 'number' &&
+    typeof summary.administrators === 'number' &&
+    typeof summary.viewers === 'number'
+  );
 }

Also applies to: 95-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/lfx-one/src/server/services/org-lens-access.service.ts` around lines 27
- 37, The isPrincipalArray function currently validates that array elements are
non-null objects, but this is insufficient since empty objects `{}` will pass
the check while lacking required fields, causing failures during later field
access. Enhance the validation in isPrincipalArray to validate the actual shape
of principal objects by reusing the same nested guard logic that
isAccessListResponse uses for validating users array elements—ensuring that each
principal element has the expected properties and structure rather than just
checking it is an object. Apply the same validation tightening at the other
location mentioned in the comment (line 95) to maintain consistency across the
codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/lfx-one/src/server/services/org-contributions.service.ts`:
- Around line 481-496: The isRowArray function currently only validates the
first element of an array, allowing corrupt rows deeper in the array to pass
validation and crash during later processing. Modify the isRowArray function to
check every element in the array using Array.every() instead of just checking
the first element, ensuring all rows match the expected contract. Additionally,
for the kpiRows validation call (the first isRowArray invocation in the guard),
require validation of specific KPI contract fields to ensure consistent cache
miss behavior when encountering corrupt or legacy row objects.

---

Outside diff comments:
In `@apps/lfx-one/src/server/services/org-lens-access.service.ts`:
- Around line 27-37: The isPrincipalArray function currently validates that
array elements are non-null objects, but this is insufficient since empty
objects `{}` will pass the check while lacking required fields, causing failures
during later field access. Enhance the validation in isPrincipalArray to
validate the actual shape of principal objects by reusing the same nested guard
logic that isAccessListResponse uses for validating users array
elements—ensuring that each principal element has the expected properties and
structure rather than just checking it is an object. Apply the same validation
tightening at the other location mentioned in the comment (line 95) to maintain
consistency across the codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9370d340-e7d5-40b9-b16f-a73e597cec83

📥 Commits

Reviewing files that changed from the base of the PR and between e1a96ee and a44e55f.

📒 Files selected for processing (5)
  • apps/lfx-one/src/server/services/org-contributions.service.ts
  • apps/lfx-one/src/server/services/org-lens-access.service.ts
  • apps/lfx-one/src/server/services/org-lens-board-committee.service.ts
  • apps/lfx-one/src/server/services/organization.service.ts
  • apps/lfx-one/src/server/services/valkey.service.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/lfx-one/src/server/services/org-lens-board-committee.service.ts
  • apps/lfx-one/src/server/services/organization.service.ts
  • apps/lfx-one/src/server/services/valkey.service.ts

Comment thread apps/lfx-one/src/server/services/org-contributions.service.ts Outdated
LFXV2-2356)

isRowArray now checks every element via every() instead of sampling the
first, and the kpiRows guard requires its full KPI contract (PROJECTS_WITH_ACTIVITY,
REPOSITORIES, COMMITS) so a single corrupt/legacy row degrades the entry to a
cache miss consistently.

Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 16, 2026 19:29
@luismoriguerra

Copy link
Copy Markdown
Contributor Author

PR thread resolution — round 2 (follow-up on a44e55f71)

Re-fetched the unresolved set via GraphQL after the prior round's push triggered bot re-review.

Threads fixed (1):

  • CodeRabbit — org-contributions.service.ts isRowArray (line ~496): Validate every cached row before accepting the cache hit. isRowArray now checks every element via every() instead of sampling only the first, and the kpiRows guard requires its full KPI contract (PROJECTS_WITH_ACTIVITY, REPOSITORIES, COMMITS). A single corrupt/legacy row now degrades the whole cache entry to a miss → direct fetch, preserving the fail-soft invariant. (85212f0bb)

Left open: none — re-fetch after the fix push shows 0 unresolved threads (CodeRabbit's re-review of the commit posted no new nits).

Quality gate (lfx-v2-ui): yarn format:check ✅ · yarn lint:check ✅ · yarn build
Self-review (/code-review on this round's diff): clean — change is minimal, module-private, preserves cache invariants (fail-soft, per-org/per-user keying, successful-results-only) and all public signatures; empty-array behavior unchanged.

Commit pushed: 85212f0bb (GPG-signed, --signoff)

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread apps/lfx-one/src/server/services/org-people-directory.service.ts Outdated
Comment thread apps/lfx-one/src/server/services/org-people-key-contacts.service.ts Outdated
Comment thread apps/lfx-one/src/server/services/org-lens-access.service.ts Outdated
…che hit (LFXV2-2356)

Tighten the Valkey accept-guards for the three Org Lens reads that serve a
cached value straight to the client: validate every row/assignment/user element
and the numeric stat/summary fields against the wire contract, so a corrupt or
legacy entry degrades to a miss (direct fetch) instead of crashing downstream on
sources spreading, name.localeCompare, or invalid stat math. Preserves the
fail-soft cache contract and public signatures.

Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@luismoriguerra

Copy link
Copy Markdown
Contributor Author

Review round 3 — recap

Re-fetched the unresolved thread set via GraphQL (reviewThreadsisResolved == false); 3 actionable threads landed on 85212f0bb, all from copilot-pull-request-reviewer, all the same class of issue: the Valkey accept-guards for cached responses served straight to the client were too permissive.

Fixed (commit 6f3e80bde):

  • org-people-directory.service.tsisAllEmployeesResponse now deep-validates every rows element, every foundations element, and the stats numeric fields, so a corrupt entry can no longer bypass computeLive and crash on sources spreading / name.localeCompare.
  • org-people-key-contacts.service.tsisKeyContactsResponse now validates the numeric stats fields (rejecting array-typed stats) and every assignments element's required string keys.
  • org-lens-access.service.tsisAccessListResponse now validates every users element and the numeric summary fields; isPrincipalArray reuses the same per-element guard.

Behavior is unchanged on the happy path; only corrupt/legacy cache entries now degrade to a miss (direct fetch), preserving the fail-soft cache contract and all public signatures.

Threads left open: none.

Quality gate: yarn format:check ✅ · yarn lint:check ✅ · yarn build ✅. No unit tests (Playwright-only); no behavior change to exercise in e2e.

Self-review (/code-review over this round's diff): no bugs/regressions/security issues found — guards are strictly tighter, empty arrays remain valid, and NaN round-trips to null (rejected).

Pushed: 85212f0bb..6f3e80bde

@MRashad26 MRashad26 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.

Automated review + code-standards-enforcer audit (report-only). 4 commits by Luis Mori Guerra — all signed off (Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>) ✅. Cursor co-author in 3 of 4 commits — not a violation ✅. No Claude co-author. ✅

🔒 Secrets / critical-constants check

No credentials, tokens, or secrets introduced. All changes are cache-infrastructure wiring: namespace strings, TTL integers, and shape-guard functions. Snowflake queries remain parameterised throughout. ✅

Overview

Routes every uncached Org Lens SSR read through the shared Valkey CachePort, using two keying models:

  • Per-org keys (withOrgCache) for caller-invariant Snowflake reads (WHERE ACCOUNT_ID = ?), 1 h TTL: overview foundations + involvement, memberships, training, contributions, events, and the People Snowflake tabs.
  • Per-user keys (withPerUserCache) for FGA/token-filtered reads, 30 s TTL: org-wide committee-seat drain, key contacts, access-list, and the merged people directory.

Also migrates OrgPeopleDirectoryService from a per-instance in-memory Map (limited to a single Express worker) onto the shared Valkey cache (cross-instance coherence). The account-context fan-out is bounded by a 8-worker cursor pool to protect the Snowflake connection pool on a cold start. 18 files, ~1 060 additions.

Correctness — verified ✅

Fail-soft / fail-closed contract preserved:

  • buildOrgCacheKey returns null when accountId is not filter-safe; buildPerUserOrgKey returns null when username OR orgUid is not filter-safe — both map to a direct-fetch bypass in valkeyService.withCache(null, ...). ✅
  • Cache faults (Redis error) degrade to the direct fetcher (handled inside ValkeyService.withCache). ✅

Per-org vs per-user segregation correct throughout:

  • withOrgCache used only on pure Snowflake reads that produce identical results for every caller of the same accountId. ✅
  • withPerUserCache used on all FGA/token-gated reads (fetchAllOrgSeats, getKeyContacts, listAccessUsers, getAccessPrincipals, getLive). The key includes getEffectiveUsername(req) so one caller's permissioned roster cannot be served to another within the TTL. ✅

people-detail:${personKey} cache bypass:

  • isFilterSafeIdentifier(personKey) checked before constructing the cache key; unsafe person keys fall through to runEmployeeDetailFetch directly, preventing : injection into the :-delimited key namespace. ✅

getOrgLensAccountContext cursor-pool fan-out:

  • cursor++ is a synchronous read-then-increment — single-threaded JS ensures no two workers can claim the same index across an await boundary. Array pre-allocation (perAccount: ...[] = new Array(uniqueAccountIds.length)) avoids holes when workers finish out-of-order. Deduplication (new Set(accountIds)) preserves the prior IN (...) collapse. ✅

fetchPagedCourseRows signature narrowed:

  • Removed the mapRow callback; now returns raw { rows: TRow[], total }. Mapping happens after the cache read, so only serialisable raw rows enter Valkey — correct pattern. ✅

OrgRosterEmployeesResult local interface deleted:

  • Was only used inside the training service; replaced cleanly by OrgRosterEmployeeRow[] plus a standalone mapRosterRows function. No shared-interface regression. ✅

Shape-guard depth is asymmetric by design:

  • Client-facing merged responses (isAllEmployeesResponse, isKeyContactsResponse, isKeyContactsStats, isAccountContextArray) validate every required field and its type. ✅
  • Intermediate raw-row arrays (isRosterRowArray, isObjectRowArray, isTraineesRaw, isEventAttendeesRaw) validate only array-of-objects shape; field-level types are enforced by downstream ?? '' / ?? 0 / ?? null guards in the mapping layer. Acceptable because raw rows are never replayed to the client without transformation.

No cross-tenant key collision:

  • Per-org namespace: {prefix}:org-lens-sf:v1:{accountId}:{subResource}accountId is validated filter-safe before inclusion. ✅
  • Per-user namespace: {prefix}:{namespace}:{username}:{orgUid} — both username and orgUid validated filter-safe. ✅
  • Schema-version suffix (:v1) on every namespace constant ensures a format change invalidates the entire keyspace without requiring an explicit flush. ✅

Code Standards Audit (~/LFX/code-enforcer-agent.md)

🔴 Critical: none.

🟡 Warning:

  • PR body contains ## Test plan with mixed ✅ / [ ] items — violates the CLAUDE.md "No test plans in PRs" rule.

🔵 Info:

  • Paged training/certification results include pagination params in the cache key (paramSignature([searchQuery, level, pageSize, offset, sortField, sortOrder])). Each unique filter combination produces a separate 1 h entry. For orgs with varied pagination patterns and large employee counts, the distinct key count per org could grow; Redis TTL eviction handles this, but there is no explicit per-org key cap. Worth monitoring post-deploy with SCAN … MATCH {prefix}:org-lens-sf:v1:{accountId}:certifications:*.
  • people-detail:{personKey} cached at 1 h. For employees with active governance roles or recent commits, the chevron-expand detail could be up to 1 h stale following a role change. Acceptable given Snowflake's daily dbt batch cadence; just worth documenting in the product runbook.
  • ORG_SEATS_NAMESPACE: 'org-seats:v1' is declared in valkey-cache.constants.ts but not consumed in this PR (the fetchAllOrgSeats cache in org-lens-board-committee.service.ts reads from the previously-introduced summary from session context). Either the constant is used in a sibling PR yet to land, or it is pre-declared for the next slice. Worth a follow-up cleanup if unused after merge.

✅ Correctly followed: ORG_LENS_ACCOUNT_CONTEXT_FETCH_CONCURRENCY added to packages/shared/src/constants/org-selector.constants.ts (correct location); all new namespace and TTL constants in packages/shared/src/constants/valkey-cache.constants.ts (correct location); no new interfaces introduced (all existing); no new files → MIT header rule N/A; no effect() calls; no template changes; all commits signed-off; Cursor co-author present (not Claude → not a violation).

Test coverage

No automated tests added. The new helpers (buildOrgCacheKey, buildPerUserOrgKey) and the deep shape guards (isAllEmployeesResponse, isKeyContactsResponse, isAccountContextArray) are strong candidates for unit tests: the key-builder null-returns on unsafe input and the field-level guard branches are both security-relevant and currently exercised only in production paths.

Verdict: PASS (one 🟡 process warning; no code defects)

@audigregorie audigregorie 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.

Code Review Summary

Large, carefully-engineered caching extension across the Org Lens server read surface. The fail-soft / fail-closed split is sound, key-safety is enforced (isFilterSafeIdentifier / isFilterSafeUsername + base64url signatures), shape-guards reject corrupt entries, and the account-context fan-out is bounded by a worker pool. Verified the contributions cache signature covers all 11 OrgContributionsQuery fields (no cache-correctness gap) and there are no cache-key collisions across the per-org namespace. No Critical issues.

Verdict: Solid. One Major to confirm (read-after-write staleness on the now-cached access list, with no invalidation on write) plus three Minor notes. Rating 4/5.

What's done well

  • Key safety is rigorous: null-key fail-closed on unsafe identifiers, base64url signatures keep :-delimited keys uncorruptible, and getEffectiveUsername scopes per-user keys to the impersonated principal so impersonation can't leak across callers.
  • Per-org (1h, shared) vs per-user (30s, isolated) split is correctly applied — the seat drain, key contacts, and access list are all token-dependent and keyed per user.
  • The account-context refactor (single IN (...) → per-account cached reads behind a bounded worker pool, with upfront dedupe) cleanly improves hit rate and protects the Snowflake connection pool.

Comment thread apps/lfx-one/src/server/services/org-lens-access.service.ts
return cached.value;
}
const username = getEffectiveUsername(req) ?? '';
return withPerUserCache(

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.

Minor — no server-side memoization when Valkey is unavailable

This service previously kept an unconditional in-memory 30s memo (DIRECTORY_CACHE_*). It now relies entirely on Valkey, so when VALKEY_URL is unset (local dev, and any deploy without Valkey) withPerUserCache degrades to a direct fetch on every call — the heaviest Org Lens operation (4-upstream merge: Snowflake + org-wide seat drain + query-service + member-service) then runs unmemoized per request. Frontend shareReplay only collapses concurrent requests, not sequential ones within the window.

Likely intended per the spec-026 cross-instance direction, but worth confirming the no-Valkey performance hit is acceptable for all targets (esp. local dev).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving this open as a deliberate-tradeoff confirmation rather than a code change. Dropping the unconditional in-memory memo is intentional with this cache migration — re-adding a process-local memo would partially reintroduce the per-instance staleness/cross-instance inconsistency the shared cache is meant to remove, and contradicts moving the directory off its in-memory Map. When VALKEY_URL is unset (local dev / any Valkey-less deploy) the directory degrades to a direct fetch per request; frontend shareReplay still collapses concurrent requests. Flagging for product/perf confirmation that the no-Valkey hit is acceptable for those targets; happy to revisit if a short-TTL local fallback is wanted.

Comment thread apps/lfx-one/src/server/services/organization.service.ts Outdated
Comment thread apps/lfx-one/src/server/services/org-lens-events.service.ts
…guards

- org-lens-access: drop the acting caller's :list/:principals per-user cache
  entries after invite/changeRole/removeUser so their own next read reflects
  the mutation instead of serving the pre-write list for up to the TTL; add a
  best-effort ValkeyService.del + invalidatePerUserCache helper (fail-soft,
  no-op on null key/disabled cache). Write-refresh bypass is preserved.
- org-lens-events: replace the shallow object-shape cache guard with contract-
  key guards (EVENT_ID for the list, CONTACT_ID for attendees/speakers) so a
  poisoned [{}] entry degrades to a miss instead of surfacing undefined ids.
- organization: harden the account-context sort against a null accountName.

Signed-off-by: Luis Mori Guerra <luismorith@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 16, 2026 20:01
@luismoriguerra

Copy link
Copy Markdown
Contributor Author

Review-thread round 4 — recap

Re-fetched the unresolved set via GraphQL (reviewThreadsisResolved==false). Found 4 open threads; fixed 3, left 1 open as a design confirmation. Commit pushed: 43443a091 (GPG-signed, signed-off).

Fixed

  • Major — access list read-after-write staleness (org-lens-access.service.ts): writes now invalidate the acting caller's :list + :principals per-user cache entries (new fail-soft ValkeyService.del + invalidatePerUserCache helper, also added to the CachePort contract). The caller's own next read reflects the mutation immediately; write-refresh bypass preserved; cross-admin entries still age out within the 30s TTL.
  • Minor — shallow cache shape-guard (org-lens-events.service.ts): split isObjectRowArray into contract-key guards — EVENT_ID for the list, CONTACT_ID for attendees/speakers — matching the depth of the people/access guards. Summary keeps the object-shape guard (all fields read null-safe).
  • Minor — accountName null sort (organization.service.ts): hardened to (a.accountName ?? '').localeCompare(b.accountName ?? '').

Left open (non-actionable this round)

  • Minor — no in-memory memo when Valkey is unavailable (org-people-directory.service.ts): intentional with the cache migration; re-adding a process-local memo would reintroduce the per-instance staleness the shared cache removes. Flagged for product/perf confirmation rather than changed. Replied on-thread.

Quality gate

  • yarn format:check ✅ · yarn lint:check ✅ · yarn build
  • Self-review (/code-review mindset) over this round's diff: clean — invalidation is fail-soft and scoped to the caller; guards reject malformed entries as a miss; no behavioral regression to the public method signatures or cache invariants. search-index.json was intentionally not staged.

@luismoriguerra luismoriguerra merged commit d9f4a90 into main Jun 16, 2026
12 checks passed
@luismoriguerra luismoriguerra deleted the feat/LFXV2-2356 branch June 16, 2026 20:07
@github-actions

Copy link
Copy Markdown

🧹 Deployment Removed

The deployment for PR #970 has been removed.

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.


const total = result.rows.length > 0 ? result.rows[0].TOTAL_RECORDS : 0;
const data = result.rows.map((row) => this.mapRowToOrgEvent(row));
const total = rows.length > 0 ? rows[0].TOTAL_RECORDS : 0;
Comment on lines +101 to +110
// Rejects a corrupt/legacy entry (degrade to a miss) before it reaches shapeResponse consumers.
private static isFoundationsResponse(value: unknown): value is OrgLensFoundationsAndProjectsResponse {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
typeof (value as OrgLensFoundationsAndProjectsResponse).accountId === 'string' &&
Array.isArray((value as OrgLensFoundationsAndProjectsResponse).rows)
);
}
Comment on lines +169 to +172
function isTraineesRaw(value: unknown): boolean {
const v = value as { traineeRows?: unknown; detailRows?: unknown; foundationRows?: unknown; courseRows?: unknown } | null;
return !!v && Array.isArray(v.traineeRows) && Array.isArray(v.detailRows) && Array.isArray(v.foundationRows) && Array.isArray(v.courseRows);
}
Comment on lines +174 to +177
function isEventAttendeesRaw(value: unknown): boolean {
const v = value as { attendeeRows?: unknown; detailRows?: unknown; foundationRows?: unknown; eventRows?: unknown } | null;
return !!v && Array.isArray(v.attendeeRows) && Array.isArray(v.detailRows) && Array.isArray(v.foundationRows) && Array.isArray(v.eventRows);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants