understand-explain
Provides a thorough, in-depth explanation of a specific file, function, or module in a codebase using a knowledge graph and source code context.
Pass rate with the skill minus without, same tasks, same model.
- 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
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 →
[](https://skillzbench.com/skills/understand-explain)[](https://skillzbench.com/skills/understand-explain)<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
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 aTypeErrorwhen a valid code is supplied.code— a promo code string. Compared case-sensitively and exactly against known codes.
Behavior by case
| Input | Result |
|---|---|
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
- Case sensitivity:
'save10'silently does nothing. Users typing codes in lowercase get no discount with no feedback. - Whitespace:
' SAVE10 'doesn't match. No.strip()is applied. - 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.
- Rounding:
round()uses banker's rounding in Python. E.g.,round(2.675, 2)gives2.67, not2.68. For money,decimal.Decimalwith explicit rounding is safer. - Float artifacts: inputs like
19.99 * 0.9can produce binary float noise before rounding; theround(..., 2)mostly masks it but not always perfectly. - Negative or zero prices: no validation.
apply_discount(-100, 'SAVE20')returns-80.0— a "discount" on a negative price increases it toward zero. - Non-numeric price:
apply_discount('100', 'SAVE10')raisesTypeError;apply_discount('100', None)returns the string'100'untouched — inconsistent typing in the return value. - Falsy-code shortcut:
if not codetreatsNoneand''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
Decimalfor currency, and decide explicitly how invalid codes should be signaled to the caller.
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
if not code: return price— ifcodeisNone, an empty string'', or any other falsy value, no discount is applied and the price is returned unchanged.SAVE10— applies a 10% discount (multiplies by 0.9) and rounds to 2 decimal places.SAVE20— applies a 20% discount (multiplies by 0.8) and rounds to 2 decimal places.- 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 — considercode.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 aTypeErrorat 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)gives2.67due to float representation. For real money handling,decimal.Decimalis 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
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:
client.incr(key)— atomically increments a counter for the given key (e.g.login:192.168.1.5orapi:user42).- If the counter is
1(first hit in this window), it sets a TTL ofwindowMsrounded up to whole seconds viaexpire. - Returns
trueif the request is allowed (count <= limit),falseif 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
INCRand key expiry (EXPIRE), which is what makes this pattern correct without locks.
Consumers
| File | Usage |
|---|---|
/routes/auth.js | Limits login attempts — typically keyed by IP +/or username to throttle brute-force/credential-stuffing attacks. |
/routes/api.js | Limits every API call — keyed per API key or user ID to enforce quotas. |
/middleware/rateLimit.js | Wraps 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
- Synchronous-looking Redis calls. Modern
node-redis(v4+) returns Promises fromincr/expire; this code treats them as synchronous, socountis a Promise,count === 1is alwaysfalse(no TTL ever set → keys never expire → permanent lockouts), andcount <= limitis alwaysfalse(everything blocked). On redis v3 the callbacks were ignored, sohitcouldn'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;
}
-
Race condition (INCR/EXPIRE not atomic together). If the process crashes between
incrandexpire, the key lives forever. The idiomatic fix is a single atomic operation:SET key 1 EX <seconds> NX+INCR, or a Lua script (INCRthenEXPIREifcount == 1), or Redis ≥7'sEXPIRE key seconds NX. -
Fixed-window burst problem. Two windows can be abused back-to-back: a client can make
limitrequests at the end of one window andlimitmore 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. -
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. -
No error handling. If Redis is down,
hitshould 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. -
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: mustawait hit()(given the async fix), mapfalse→429withRetry-After, and decide the key strategy (IP behind proxies requiresreq.ipwithtrust proxyconfigured, 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.
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:6379by default, and a Redis outage will surface as unhandled errors.
function hit(key, limit, windowMs) {
const count = client.incr(key);
INCRatomically increments a Redis counter, creating it at1if 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 === 1is the signal "I created this key," so only one caller pays theEXPIREcost. windowMsis in milliseconds; Redis TTLs are in seconds, henceMath.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 = 5allows 5 requests; the 6th returnsfalse.
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
- Non-atomic INCR + EXPIRE. If the process crashes (or Redis restarts) between
INCRreturning 1 andEXPIREexecuting, the key exists without a TTL and the client is permanently locked out until manual deletion. The standard fix is a Lua script orMULTI/EXECso increment-and-expire happen atomically, or Redis 7'sEXoption onSET. - Synchronous-looking, actually async. The
redisclient'sincr()returns a promise/callback-based result, not the count.const count = client.incr(key)will be a Promise (or undefined depending on client version), socount === 1is almost never true andcount <= limitcompares a Promise object — meaning this function as written likely always returns truthy and never sets the TTL. It needsawait client.incr(key)andasync function hit(...). This is the single biggest bug in the module. - No key namespacing. Callers must pass distinct keys (e.g.,
login:ip:1.2.3.4vsapi:user:42), otherwise a login limit and an API limit share a counter. - 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
hitafter 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
What it does
fetch_with_retry performs an HTTP GET with exponential backoff retry, returning parsed JSON on success.
Control flow
- Loop up to
retries(default 3) times. - On each attempt:
session.get(url)opens a request; theasync withensures the response/connection is released when the block exits.- HTTP 429 (rate limited): sleeps
2 ** attemptseconds (1s, 2s, 4s...) andcontinues to the next attempt. Note: this consumes an attempt but doesn't count as an error. - Other non-2xx statuses:
raise_for_status()throwsClientResponseError(a subclass ofClientError), which is caught below. - Success (2xx): returns
await resp.json()immediately.
- On
aiohttp.ClientError(network failures, timeouts, 4xx/5xx fromraise_for_status):- If it's the last attempt, re-raises the exception.
- Otherwise sleeps
2 ** attemptand retries.
- Fallthrough
return None: only reachable if all attempts got 429s — the loop exhausts without raising or returning. So a persistent rate-limit yieldsNone, not an exception. Callers must handle bothNoneand exceptions.
Failure behavior summary
| Scenario | Result |
|---|---|
| Success | Parsed JSON |
| 429 every time | None (silent failure!) |
| ClientError on final attempt | Exception raised |
| ClientError earlier | Backoff, retry |
Things to watch when adding a timeout
- No timeout exists today —
session.getuses the session's default (often 5 min in aiohttp). Wrap withasync with session.get(url, timeout=aiohttp.ClientTimeout(total=10))or set it on the session.asyncio.TimeoutErroris not a subclass ofClientError, so it would currently escape the retry loop — catch it explicitly (or useaiohttp.ClientTimeout, which raisesasyncio.TimeoutError; add it to the except clause). - 429 doesn't respect
Retry-After— consider honoring that header instead of fixed exponential backoff. - The
return Nonefallthrough is easy to miss; you may want it to raise instead so callers have one failure mode.
(no text deliverable)
understand-explain--m4
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
- 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. - 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.
time.time()as the clock. Wall-clock time, not a monotonic clock.- No locking, no thread-safety primitives. It relies on CPython's GIL making single dict operations effectively atomic.
- Module-level singleton.
_cacheis shared by every importer ofcache.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, nocachetools) 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()orclear(). 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
- 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 —getonly evicts the key it's asked about. Underset()-heavy traffic with high-cardinality keys (e.g., per-user or per-URL keys),_cachegrows without limit until the process OOMs. This is the most serious defect. - 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. - 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. - Thread-safety is implicit, not guaranteed. The
getpath does read-then-pop, andsetdoes 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. - 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.
- 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.
- No
delete/invalidateAPI. After a data update, the stale value persists up to 60s with no way to purge it except overwriting the exact key. - 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
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 orNoneif absent/expiredset(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
-
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),
_cachegrows without limit → eventual OOM. This is the module's most serious risk. -
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()orclear()function at all. Staleness is bounded only by the TTL. -
Middleware response caching is the highest-stakes consumer.
api/middleware.pycaching 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).
-
No locking / race conditions. Under threads (e.g., a threaded WSGI server), concurrent
get/seton 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 concurrentsetcan, in a narrow window, evict a freshly written entry. -
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. -
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. -
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.
-
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
ttlexplicitly.
What depends on it
| Consumer | Use | Risk profile |
|---|---|---|
services/user.py | Profile lookups | Moderate: stale profiles for up to 60s after an update; no invalidation hook on profile writes. |
services/product.py | Product details | Usually lowest risk (product data changes slowly); benefits most from the cache. |
api/middleware.py | Full anonymous responses | Highest 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)andclear()for invalidation on writes. - Add a
- 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.