Sign inGet started
–

Provides a thorough, in-depth explanation of a specific file, function, or module in a codebase using a knowledge graph and source code context.

Lift · GLM-5.3-flash−75ppdirectional · all models →
85
Trustsecurity · quality · license · upkeep
Scan Saferisk 0/100
LicenseMITserved via MCP
Claude Haiku 5.5 · GPT-6 Luna — not run yet
Pass-rate Lift−75pp95% CI [−100, −25] · CI below zero
screen tier · directionalchat harness (v2)
−100pp+100pp

Pass rate with the skill minus without, same tasks, same model.

baseline
75%
with skill
0%
forced
50%
Content Lift forced arm
−25pp [−75, 0]
Trigger rate
100%
Trials
12 · 4 tasks
Model
GLM-5.3-flash
Measured
2026-10-15

Content Lift = the skill's instructions injected into the system prompt, so it measures what the content does once loaded, apart from whether the agent chooses to load it. Method →

Embed this score · live badge for your README
SkillzBench badge preview

The badge shows the real benchmark number and updates itself when we re-bench — including the honest states (“did not trigger”, “not benched yet”). All badge options →

Markdown[![SkillzBench](https://skillzbench.com/api/badge/understand-explain.svg)](https://skillzbench.com/skills/understand-explain)
+ cost/task[![SkillzBench](https://skillzbench.com/api/badge/understand-explain.svg?cost=1)](https://skillzbench.com/skills/understand-explain)
HTML<a href="https://skillzbench.com/skills/understand-explain"><img src="https://skillzbench.com/api/badge/understand-explain.svg" alt="SkillzBench benchmark" height="20"></a>

A/B outputs — median trial per arm

Chat harness (v2): the agent's final message without the skill (left) vs with it (right).

understand-explain--m1
baseline · 0.85

What it does

apply_discount(price, code) returns a discounted price if code matches a known promo code; otherwise it returns the price unchanged.

Parameters

  • price — the original price (expected to be a number, typically a float). It's used directly in arithmetic, so passing a non-numeric type would raise a TypeError when a valid code is supplied.
  • code — a promo code string. Compared case-sensitively and exactly against known codes.

Behavior by case

InputResult
code is falsy (None, '')price returned unchanged
code == 'SAVE10'round(price * 0.9, 2) — 10% off, rounded to 2 decimals
code == 'SAVE20'round(price * 0.8, 2) — 20% off, rounded to 2 decimals
Any other code ('save10', 'SAVE15', etc.)price returned unchanged — no error raised

Edge cases to be aware of

  1. Case sensitivity: 'save10' silently does nothing. Users typing codes in lowercase get no discount with no feedback.
  2. Whitespace: ' SAVE10 ' doesn't match. No .strip() is applied.
  3. Unknown codes are silent: an invalid code behaves the same as no code. If the checkout UI promises "invalid code" feedback, this function can't provide it — you'd need it to return a status or raise.
  4. Rounding: round() uses banker's rounding in Python. E.g., round(2.675, 2) gives 2.67, not 2.68. For money, decimal.Decimal with explicit rounding is safer.
  5. Float artifacts: inputs like 19.99 * 0.9 can produce binary float noise before rounding; the round(..., 2) mostly masks it but not always perfectly.
  6. Negative or zero prices: no validation. apply_discount(-100, 'SAVE20') returns -80.0 — a "discount" on a negative price increases it toward zero.
  7. Non-numeric price: apply_discount('100', 'SAVE10') raises TypeError; apply_discount('100', None) returns the string '100' untouched — inconsistent typing in the return value.
  8. Falsy-code shortcut: if not code treats None and '' identically, which is probably intended, but also means a code object with a weird __bool__ would be skipped.

Suggested refactor direction (if you modify it)

  • Normalize input: code = (code or '').strip().upper()
  • Use a dict mapping codes → discount factors for easy extension
  • Consider Decimal for currency, and decide explicitly how invalid codes should be signaled to the caller.
with skill · 0.64

I don't have access to a project knowledge graph in this environment, so here's a direct explanation based on the code you shared.

What apply_discount does

It takes a price and an optional discount code, and returns the price after applying the discount — or the original price if no valid code is given.

def apply_discount(price, code):
    if not code:
        return price
    if code == 'SAVE10':
        return round(price * 0.9, 2)
    if code == 'SAVE20':
        return round(price * 0.8, 2)
    return price

Parameters

  • price — the original price (expected to be a number, typically a float). It's never validated; whatever is passed is multiplied directly.
  • code — the discount code string. It's treated as case-sensitive and must match exactly.

Behavior, step by step

  1. if not code: return price — if code is None, an empty string '', or any other falsy value, no discount is applied and the price is returned unchanged.
  2. SAVE10 — applies a 10% discount (multiplies by 0.9) and rounds to 2 decimal places.
  3. SAVE20 — applies a 20% discount (multiplies by 0.8) and rounds to 2 decimal places.
  4. Fallthrough return price — any other non-empty code (e.g., 'SAVE15', 'save10', ' SAVE10 ') is silently ignored and the price is returned unchanged.

Edge cases and gotchas to know before modifying

  • Case sensitivity: 'save10' or 'Save10' gets no discount, silently. Same for leading/trailing whitespace (' SAVE10 '). If users type codes manually, this is a common bug source — consider code.strip().upper() if that's desired.
  • Invalid codes fail silently: there's no error or signal distinguishing "no code" from "bad code." Callers can't tell whether a discount was applied without re-checking the result.
  • No validation of price: negative prices, None, strings, or non-numeric types will either produce odd results or raise a TypeError at the multiplication step. apply_discount(None, 'SAVE10') raises; apply_discount(-100, 'SAVE10') returns -90.0 (a "discount" on a negative price).
  • Rounding: round(x, 2) uses banker's rounding in Python (round-half-to-even), e.g. round(2.675, 2) gives 2.67 due to float representation. For real money handling, decimal.Decimal is safer than floats.
  • Rounding only on discount paths: the no-discount return paths don't round, so the function can return an unrounded value when no code matches — inconsistent if callers expect 2-decimal output.
  • Extensibility: adding new codes means editing the function body. A dict lookup ({'SAVE10': 0.9, 'SAVE20': 0.8}) would be cleaner and easier to extend or move to configuration.

Suggested refactor (if you're modifying it anyway)

DISCOUNTS = {'SAVE10': 0.9, 'SAVE20': 0.8}

def apply_discount(price, code):
    if not code:
        return price
    factor = DISCOUNTS.get(code.strip().upper())
    if factor is None:
        return price
    return round(price * factor, 2)

This keeps behavior compatible for the existing codes while fixing the case/whitespace gotcha and making new codes a one-line addition.

understand-explain--m2
baseline · 0.85

rate-limiter.js — Deep Dive

What the module does

It exports a single function, hit(key, limit, windowMs), implementing a classic fixed-window rate limiter backed by Redis:

  1. client.incr(key) — atomically increments a counter for the given key (e.g. login:192.168.1.5 or api:user42).
  2. If the counter is 1 (first hit in this window), it sets a TTL of windowMs rounded up to whole seconds via expire.
  3. Returns true if the request is allowed (count <= limit), false if the limit is exceeded.

Dependencies

  • redis (node-redis client) — the only external dependency. Redis is chosen because rate-limit state must be shared across processes/instances (a single Node process's memory wouldn't work behind a load balancer or with cluster mode).
  • Redis itself — provides the atomic INCR and key expiry (EXPIRE), which is what makes this pattern correct without locks.

Consumers

FileUsage
/routes/auth.jsLimits login attempts — typically keyed by IP +/or username to throttle brute-force/credential-stuffing attacks.
/routes/api.jsLimits every API call — keyed per API key or user ID to enforce quotas.
/middleware/rateLimit.jsWraps hit() in an Express middleware, presumably returning 429 Too Many Requests when hit() returns false, and attaching headers like Retry-After / X-RateLimit-*.

The middleware is the architectural glue: instead of each route calling hit() directly, routes can be mounted behind app.use(rateLimit(...)), centralizing key construction, limits, and rejection behavior.

Correctness and design issues worth knowing

  1. Synchronous-looking Redis calls. Modern node-redis (v4+) returns Promises from incr/expire; this code treats them as synchronous, so count is a Promise, count === 1 is always false (no TTL ever set → keys never expire → permanent lockouts), and count <= limit is always false (everything blocked). On redis v3 the callbacks were ignored, so hit couldn't reliably report results either. This module is almost certainly broken as written and should be:
async function hit(key, limit, windowMs) {
  const count = await client.incr(key);
  if (count === 1) await client.expire(key, Math.ceil(windowMs / 1000));
  return count <= limit;
}
  1. Race condition (INCR/EXPIRE not atomic together). If the process crashes between incr and expire, the key lives forever. The idiomatic fix is a single atomic operation: SET key 1 EX <seconds> NX + INCR, or a Lua script (INCR then EXPIRE if count == 1), or Redis ≥7's EXPIRE key seconds NX.

  2. Fixed-window burst problem. Two windows can be abused back-to-back: a client can make limit requests at the end of one window and limit more immediately at the start of the next — up to 2× the intended rate in a short burst. Sliding-window or token-bucket (e.g. redis-cell, or a Lua sliding log) fixes this.

  3. Math.ceil(windowMs / 1000) loses sub-second precision — a 500 ms window becomes 1 s. Also, the TTL starts at the first hit, not at a wall-clock boundary, so "windows" drift per key.

  4. No error handling. If Redis is down, hit should fail open (allow) or closed (block) as an explicit policy decision; here an exception would bubble into the Express middleware and likely produce a 500.

  5. Shared client, no config. redis.createClient() with no URL/port options assumes a local default Redis; there's no reconnect strategy, no key prefix/namespace, and one global client for the whole app.

What the consumers must handle

  • /middleware/rateLimit.js: must await hit() (given the async fix), map false → 429 with Retry-After, and decide the key strategy (IP behind proxies requires req.ip with trust proxy configured, or an API key/user ID).
  • /routes/auth.js: should key on both IP and username (so one attacker can't lock out a victim's login), and ideally apply escalating lockouts after repeated failures.
  • /routes/api.js: needs per-credential keys and should return quota headers so well-behaved clients can back off.

Bottom line: the design (Redis INCR + EXPIRE fixed-window limiter, wrapped in Express middleware, used by auth and API routes) is a standard, sound pattern — but the implementation as shown is non-functional due to the synchronous misuse of the async redis client, and should also be hardened with atomic INCR+EXPIRE, sliding windows, and explicit Redis-failure behavior.

with skill · 0.64

There's no knowledge graph available in this environment, so I'll explain based on the source you provided plus the dependency structure you described.

rate-limiter.js — In-Depth Explanation

What it is

A tiny fixed-window rate limiter backed by Redis. It exposes a single function, hit(key, limit, windowMs), which answers one question: "Has this key exceeded its allowed number of actions within this time window?" It returns true if the action is allowed, false if it should be throttled.

Line-by-line

const redis = require('redis');
const client = redis.createClient();
  • Creates a module-level Redis client. Because it's created at require-time and shared via module.exports, all consumers share one connection — this is intentional and important: the counter lives in Redis, not in process memory, so the limit is enforced consistently across multiple Node processes/workers behind a load balancer.
  • Note: no connection options (host/port/password) and no error handler are attached — the client connects to localhost:6379 by default, and a Redis outage will surface as unhandled errors.
function hit(key, limit, windowMs) {
  const count = client.incr(key);
  • INCR atomically increments a Redis counter, creating it at 1 if it doesn't exist. Atomicity matters: even with many concurrent requests, no two callers can race and miss an increment.
  if (count === 1) client.expire(key, Math.ceil(windowMs / 1000));
  • The first caller in a window sets a TTL so the key auto-expires after the window. count === 1 is the signal "I created this key," so only one caller pays the EXPIRE cost.
  • windowMs is in milliseconds; Redis TTLs are in seconds, hence Math.ceil(... / 1000) — a sub-second window rounds up to 1 second.
  return count <= limit;
}
  • The Nth request within the window is allowed if N ≤ limit. So limit = 5 allows 5 requests; the 6th returns false.
module.exports = { hit };
  • Named export; consumers do const { hit } = require('./rate-limiter').

Algorithm: fixed-window counter

This is the simplest rate-limiting pattern. Each key gets a counter that resets when its TTL lapses. Its known weakness is the boundary burst problem: a client can make limit requests at the very end of one window and limit more immediately at the start of the next — up to 2× the intended rate in a short burst. (Alternatives: sliding-window log, sliding-window counter via two buckets, or token bucket — e.g., rate-limiter-flexible or a Lua script doing INCR + EXPIRE atomically.)

Correctness issues worth knowing

  1. Non-atomic INCR + EXPIRE. If the process crashes (or Redis restarts) between INCR returning 1 and EXPIRE executing, the key exists without a TTL and the client is permanently locked out until manual deletion. The standard fix is a Lua script or MULTI/EXEC so increment-and-expire happen atomically, or Redis 7's EX option on SET.
  2. Synchronous-looking, actually async. The redis client's incr() returns a promise/callback-based result, not the count. const count = client.incr(key) will be a Promise (or undefined depending on client version), so count === 1 is almost never true and count <= limit compares a Promise object — meaning this function as written likely always returns truthy and never sets the TTL. It needs await client.incr(key) and async function hit(...). This is the single biggest bug in the module.
  3. No key namespacing. Callers must pass distinct keys (e.g., login:ip:1.2.3.4 vs api:user:42), otherwise a login limit and an API limit share a counter.
  4. No Redis-failure fallback. If Redis is down, every call throws — callers should decide whether to fail open (allow) or closed (block).

Its dependents

/middleware/rateLimit.js — Express wrapper

This is the integration layer. It presumably exports something like:

const { hit } = require('../rate-limiter');
function rateLimit({ limit, windowMs, keyFn }) {
  return async (req, res, next) => {
    const key = keyFn ? keyFn(req) : req.ip;
    const allowed = await hit(key, limit, windowMs);
    if (allowed) return next();
    res.status(429).json({ error: 'Too many requests' });
  };
}

Typical responsibilities: deriving the key (IP, user ID, API key), mapping false → HTTP 429 with Retry-After, and being mounted per-route or app-wide. Because hit is shared, all middleware instances share the same Redis client — one connection per process, not per route.

/routes/auth.js — login attempts

Uses hit keyed on something like login:ip:<ip> or login:user:<email> to throttle brute-force password guessing. Typical config: a small limit (e.g., 5) over a longer window (e.g., 15 min). Two subtleties:

  • Keying only by IP punishes users behind NAT/corporate proxies; keying only by account lets attackers rotate IPs. Many implementations use both.
  • A common pattern is to only count failed attempts (call hit after a failed login, or reset the key on success) rather than every attempt.

/routes/api.js — all API calls

Uses hit keyed per API key/user with a higher limit (e.g., 100/min) for quota enforcement. Expect 429 responses plus headers like X-RateLimit-Limit / X-RateLimit-Remaining (which would require hit to also return the current count — a common extension is returning { allowed, count, remaining, resetAt }).

Dependency graph

routes/auth.js ──────�
understand-explain--m3
baseline · 1.00

What it does

fetch_with_retry performs an HTTP GET with exponential backoff retry, returning parsed JSON on success.

Control flow

  1. Loop up to retries (default 3) times.
  2. On each attempt:
    • session.get(url) opens a request; the async with ensures the response/connection is released when the block exits.
    • HTTP 429 (rate limited): sleeps 2 ** attempt seconds (1s, 2s, 4s...) and continues to the next attempt. Note: this consumes an attempt but doesn't count as an error.
    • Other non-2xx statuses: raise_for_status() throws ClientResponseError (a subclass of ClientError), which is caught below.
    • Success (2xx): returns await resp.json() immediately.
  3. On aiohttp.ClientError (network failures, timeouts, 4xx/5xx from raise_for_status):
    • If it's the last attempt, re-raises the exception.
    • Otherwise sleeps 2 ** attempt and retries.
  4. Fallthrough return None: only reachable if all attempts got 429s — the loop exhausts without raising or returning. So a persistent rate-limit yields None, not an exception. Callers must handle both None and exceptions.

Failure behavior summary

ScenarioResult
SuccessParsed JSON
429 every timeNone (silent failure!)
ClientError on final attemptException raised
ClientError earlierBackoff, retry

Things to watch when adding a timeout

  • No timeout exists today — session.get uses the session's default (often 5 min in aiohttp). Wrap with async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) or set it on the session. asyncio.TimeoutError is not a subclass of ClientError, so it would currently escape the retry loop — catch it explicitly (or use aiohttp.ClientTimeout, which raises asyncio.TimeoutError; add it to the except clause).
  • 429 doesn't respect Retry-After — consider honoring that header instead of fixed exponential backoff.
  • The return None fallthrough is easy to miss; you may want it to raise instead so callers have one failure mode.
with skill · 0.00
(no text deliverable)
understand-explain--m4
baseline · 0.64

cache.py — In-Depth Explanation

Design Overview

This is a minimal in-process, in-memory TTL cache built on a plain Python dict. It has exactly two operations:

  • get(key) — looks up the key, checks expiry, and lazily evicts expired entries.
  • set(key, value, ttl=60) — stores a value with an absolute expiry timestamp (time.time() + ttl).

Each entry is a dict {'value': ..., 'expires': ...} — a tiny record rather than a tuple, which keeps the code self-documenting at the cost of a few bytes per entry.

Key design properties

  1. Lazy eviction, not background cleanup. Expired entries are only removed when someone calls get() on that exact key. There is no sweeper thread, no size cap, no LRU logic. This is the simplest possible correct-enough approach.
  2. Absolute (not sliding) TTL. The expiry is fixed at write time. Reading an entry does not refresh it. This gives predictable staleness bounds: a cached value can never be older than its TTL.
  3. time.time() as the clock. Wall-clock time, not a monotonic clock.
  4. No locking, no thread-safety primitives. It relies on CPython's GIL making single dict operations effectively atomic.
  5. Module-level singleton. _cache is shared by every importer of cache.py — one namespace, one cache, all callers share the same pool.

Why This Approach

  • Simplicity over features. For a small service, a 15-line cache avoids dependencies (no redis, no cachetools) and deployment complexity. It's "good enough" caching: it absorbs repeated reads within a 60-second window.
  • In-process = zero network latency. Reads are dict lookups (~microseconds), versus milliseconds for an external cache. For hot keys like product details, this is a big win.
  • TTL as the only invalidation strategy. Manual invalidation is conspicuously absent — there's no delete() or clear(). The design implicitly accepts up to 60s of staleness in exchange for never having to reason about invalidation correctness. That's a deliberate trade: it works well for data that's cheap to be slightly wrong about, and badly for anything that must be consistent.
  • Single shared cache. One dict for all consumers keeps memory bounded to one process and makes the code trivial to reason about — but see the coupling issues below.

Failure Modes

  1. Unbounded memory growth. Keys that are written but never read again (or read only after being overwritten) are never evicted by get()'s lazy path — get only evicts the key it's asked about. Under set()-heavy traffic with high-cardinality keys (e.g., per-user or per-URL keys), _cache grows without limit until the process OOMs. This is the most serious defect.
  2. Not multi-process safe / not shared. Under gunicorn/uwsgi with multiple workers, each worker has its own _cache. Effective hit rate drops by ~1/N, and — worse — different workers can serve different cached responses for the same key for up to 60s.
  3. Wall-clock sensitivity. time.time() jumps backward on NTP corrections or VM migrations; a backward jump extends entry lifetimes. A monotonic clock (time.monotonic()) would be correct here.
  4. Thread-safety is implicit, not guaranteed. The get path does read-then-pop, and set does read-modify-write of the dict. Under GIL these are individually atomic, but a future switch to free-threaded Python, or composite logic built on top (check-then-set patterns), introduces races. There's also no protection against one thread seeing a value the instant another replaces it — benign here, but fragile as a foundation.
  5. No stampede protection. When a hot key expires, every concurrent request misses simultaneously and all hit the backing store (DB/API) at once. With no lock-and-single-flight pattern, a popular product expiring can cause a thundering herd.
  6. No serialization. Values are stored by reference. If a caller mutates a returned object (e.g., appends to a cached list), it corrupts the cache for everyone. Mutable cached values are a latent bug factory.
  7. No delete/invalidate API. After a data update, the stale value persists up to 60s with no way to purge it except overwriting the exact key.
  8. No metrics, no namespacing. Impossible to tell hit rate or which consumer is filling the cache; key collisions between consumers are possible if key formats overlap (mitigated only by convention).

What Depends on It — and How

Three consumers, with very different tolerance for this cache's weaknesses:

services/user.py — profile lookups

  • Sensitivity: moderate. Profile data changes occasionally; 60s staleness is usually acceptable (user sees their own edit lag briefly).
  • Risk: if keys are per-user-ID, this is a high-cardinality writer — the unbounded-growth failure mode (#1) applies directly. A large user base with 60s TTLs can accumulate hundreds of thousands of dead entries that are never read again.
  • Risk: if profile objects are mutable and callers mutate them, cross-user data corruption is possible (#6).

services/product.py — product details

  • Sensitivity: low-to-moderate. Product data is fairly static; 60s staleness is fine, and price changes lagging a minute is typically tolerable.
  • Risk: cache stampede (#5) is most dangerous here — popular products are hot keys, and their expiry will synchronously hammer the product DB. This is the consumer most likely to have caused an incident.
  • Risk: shared keyspace — product keys must be namespaced (e.g., product:{id}) or they could collide with user keys.

api/middleware.py — full anonymous response caching

  • Sensitivity: highest. Caching entire responses by (presumably) URL means:
    • Correctness risk: any endpoint that isn't truly anonymous-safe (sets cookies, varies by header, has side effects, returns user-specific data leaked into the body) will serve wrong responses to the wrong users. The cache has no concept of Vary headers or auth
with skill · 0.64

I don't have access to a shell or a knowledge graph in this environment, so I'll explain the module directly from the source you provided.

cache.py — In-Depth Explanation

What it is

A minimal in-process, in-memory TTL cache — roughly 15 lines, no dependencies, no configuration. It exposes exactly two functions:

  • get(key) — returns the cached value or None if absent/expired
  • set(key, value, ttl=60) — stores a value with an absolute expiry timestamp

Design and why this approach

1. Dictionary + expiry timestamp (lazy expiration). Each entry is {'value': ..., 'expires': time.time() + ttl}. Expiration is lazy: entries are only removed when a get() touches them and finds them stale. There is no background sweeper thread. This is the classic "check expiry on read" pattern — cheap, simple, and correct for reads, at the cost of dead entries lingering in memory until they're next requested (or forever, if never requested again).

2. Why in-process rather than Redis/Memcached?

  • Zero infrastructure: no network hop, no serialization, no client library.
  • Latency is a dict lookup (~microseconds), ideal for hot paths like profile/product lookups and anonymous-page caching.
  • The tradeoff: the cache is per-process. Under multiple workers (gunicorn/uwsgi processes), each worker has its own _cache, so hit rates are diluted and different workers can serve different (stale) versions of the same key. There's also no shared invalidation — clearing the cache in one worker doesn't clear it elsewhere.

3. Why a module-level dict instead of a class? It's a singleton by import semantics: from cache import get, set gives every caller the same dict. Simple, but it also means state is global — hard to reset in tests except via cache._cache.clear() (touching a private).

4. None as the miss sentinel. get() returns None on miss. This is a deliberate simplification but a real footgun: callers cannot cache None as a legitimate value (e.g., caching "user has no profile" / negative caching). A get() of a cached None is indistinguishable from a miss, so callers will re-fetch every time. A contains()/get_or_set() API would avoid this.

Failure modes

  1. Unbounded memory growth / leak. No max size, no eviction policy (no LRU). Keys that are written but never read again (or read rarely) are never lazily expired. With high-cardinality keys (e.g., caching per-user or per-URL), _cache grows without limit → eventual OOM. This is the module's most serious risk.

  2. Stale data window. A value cached at t with TTL 60 is served until t+60. Any caller that mutates underlying data (e.g., a user updates their profile) has no way to invalidate the cache — there's no delete() or clear() function at all. Staleness is bounded only by the TTL.

  3. Middleware response caching is the highest-stakes consumer. api/middleware.py caching whole responses for anonymous requests means:

    • Any bug where an authenticated request is misclassified as anonymous leaks one user's page to another (a classic cache-poisoning/authorization bug class).
    • Set-Cookie headers, CSRF tokens, or per-request nonces baked into cached HTML will be replayed to other users.
    • If response keys include query strings, the cardinality problem in #1 becomes acute (cache-filling DoS: an attacker sends many unique query strings, each cached, exhausting memory).
  4. No locking / race conditions. Under threads (e.g., a threaded WSGI server), concurrent get/set on the same key is mostly safe in CPython due to the GIL and dict atomicity, but you can still get stampedes: many threads miss simultaneously and all hit the database. There's no single-flight/dogpile protection. Also, pop(key, None) racing with a concurrent set can, in a narrow window, evict a freshly written entry.

  5. time.time() is not monotonic. If the system clock jumps (NTP correction, VM migration), expiries can be wrong — entries live too long or die instantly. time.monotonic() would be the correct choice for TTL math.

  6. No serialization / mutation hazard. Values are stored by reference. If a caller mutates a retrieved object in place (e.g., appends to a cached list, or mutates a cached dict that's also the live model object), the cache is silently corrupted for all future readers. Similarly, callers who set() a mutable object and then mutate it corrupt the cache retroactively.

  7. No observability. No hit/miss counters, no logging, no size metrics — you can't tell whether the cache is helping or how big it is.

  8. TTL=60 default is a global coupling point. All three consumers share the same default; profile data, product data, and full HTML responses all go stale on the same clock, with no per-consumer tuning unless callers pass ttl explicitly.

What depends on it

ConsumerUseRisk profile
services/user.pyProfile lookupsModerate: stale profiles for up to 60s after an update; no invalidation hook on profile writes.
services/product.pyProduct detailsUsually lowest risk (product data changes slowly); benefits most from the cache.
api/middleware.pyFull anonymous responsesHighest risk: correctness depends on "anonymous-only" classification being airtight; cardinality-driven memory growth; any per-user data leaking into "anonymous" responses is a security bug.

Because all three import the same singleton, a change to cache.py (e.g., adding eviction, changing get's return contract, or adding serialization) affects every layer at once — data layer, service layer, and HTTP edge.

If you were to harden it

  • Add delete(key) and clear() for invalidation on writes.
  • Add a
Limitations
  • Results are for the SkillzBench reference agent on GLM-5.3-flash. Switch models above — a skill can help one model and not another.
  • Screen tier (1 trial per arm) is directional. Verified needs a full-tier run (≥ 8 tasks, ≥ 3 trials per arm) with the CI excluding zero.
  • Task coverage: 4 generated tasks for this skill's category; a skill can shine elsewhere.