<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tsc on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/tsc/</link><description>Recent content in Tsc on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Wed, 15 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/tsc/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search-demo Dev Log #14 — Clock Skew, S3-First Ref Cache, Attribute-Aware Injection</title><link>https://ice-ice-bear.github.io/posts/2026-04-15-hybrid-search-dev14/</link><pubDate>Wed, 15 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-04-15-hybrid-search-dev14/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search-demo Dev Log #14 — Clock Skew, S3-First Ref Cache, Attribute-Aware Injection" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;Short but sharp week. Five commits, all production-hardening: a Google OAuth clock-skew fix blocking logins, ecosystem.config.js made host-portable for PM2 across dev/prod, the reference-image key cache moved off local filesystem onto S3 (so dev and prod see the same state), attribute-aware model auto-injection wired up, and personas relabeled with a 3-shot prompt that adds age estimates.&lt;/p&gt;
&lt;p&gt;Previous: &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-13-hybrid-search-dev13/" &gt;hybrid-image-search-demo Dev Log #13&lt;/a&gt;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 A["Login: Invalid token 'used too early'"] --&gt; B["clock_skew_in_seconds=10"]
 C["ref cache from local fs"] --&gt; D[ref cache from S3]
 E[model auto-injection: any image] --&gt; F[attribute-aware injection by tag]
 G[personas: old labels] --&gt; H[3-shot relabel + age estimates]&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="google-oauth-clock-skew"&gt;Google OAuth clock skew
&lt;/h2&gt;&lt;h3 id="context"&gt;Context
&lt;/h3&gt;&lt;p&gt;Login blocked with &lt;code&gt;Invalid Google token: Token used too early, 1776217862 &amp;lt; 1776217863. Check that your computer's clock is set correctly.&lt;/code&gt; The server&amp;rsquo;s clock was ~1 second ahead of Google&amp;rsquo;s — the JWT &lt;code&gt;iat&lt;/code&gt; was in the future from the server&amp;rsquo;s perspective.&lt;/p&gt;
&lt;h3 id="fix"&gt;Fix
&lt;/h3&gt;&lt;p&gt;Added &lt;code&gt;clock_skew_in_seconds=10&lt;/code&gt; to &lt;code&gt;id_token.verify_oauth2_token(...)&lt;/code&gt; in &lt;code&gt;backend/src/auth.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;id_token&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verify_oauth2_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;google_requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;clock_skew_in_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Resolved immediately. A server should never trust its own clock to the second against a third party&amp;rsquo;s &lt;code&gt;iat&lt;/code&gt; — 10 seconds of tolerance is standard practice for JWT validation and doesn&amp;rsquo;t open any meaningful attack surface.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="s3-first-reference-key-cache"&gt;S3-first reference key cache
&lt;/h2&gt;&lt;h3 id="context-1"&gt;Context
&lt;/h3&gt;&lt;p&gt;The model/reference-image cache was built from the local filesystem. This broke in production because prod&amp;rsquo;s S3-mounted paths didn&amp;rsquo;t always reflect the latest uploads, and because dev and prod had divergent local filesystem state. When a user regenerated in &amp;ldquo;tone only&amp;rdquo; mode, the UI showed the wrong reference image because the path resolved against local state, not S3 reality.&lt;/p&gt;
&lt;h3 id="fix-1"&gt;Fix
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;ce33906 fix(storage): build ref key cache from S3, not local filesystem&lt;/code&gt; — cache construction now enumerates S3 objects directly. All image retrieval paths resolved against S3 keys. Also backfilled existing generation history so old records point to the correct S3 URL.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="attribute-aware-model-auto-injection"&gt;Attribute-aware model auto-injection
&lt;/h2&gt;&lt;h3 id="context-2"&gt;Context
&lt;/h3&gt;&lt;p&gt;Previous injection logic would pull &lt;em&gt;any&lt;/em&gt; image matching a loose condition, so the comparison mode (&amp;ldquo;tone + angle&amp;rdquo; vs &amp;ldquo;tone only&amp;rdquo;) sometimes injected a model image that didn&amp;rsquo;t match the tagged attribute. Users saw the wrong reference in the output grid.&lt;/p&gt;
&lt;h3 id="fix-2"&gt;Fix
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;d492ee1 feat(gen): attribute-aware model auto-injection&lt;/code&gt; — injection now keys on the tagged attributes (angle, tone) of the requested model folder. Subfolders under &lt;code&gt;s3://diffs-studio-hybrid-search/.../01. Model&lt;/code&gt; are treated as attribute groups, one reference per group.&lt;/p&gt;
&lt;p&gt;Pre-requisite: each model reference was relabeled so attributes are trustworthy. Grouping by folder means labels are a filesystem-visible schema, not a DB column, which matters because the ops team can audit and edit labels with just S3 browsing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="persona-relabeling-with-3-shot-prompt--age"&gt;Persona relabeling with 3-shot prompt + age
&lt;/h2&gt;&lt;h3 id="context-3"&gt;Context
&lt;/h3&gt;&lt;p&gt;Persona labels were set earlier with a zero-shot prompt and did not include age estimates. User-facing filters needed age granularity.&lt;/p&gt;
&lt;h3 id="fix-3"&gt;Fix
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;2743eaf chore(labels): re-label personas with 3-shot prompt and age estimates&lt;/code&gt; — re-ran the labeler with three in-context examples per request and an age-range field. Labels pushed to the repo so every server picks them up, avoiding per-instance label drift.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pm2--tsc-fixes"&gt;PM2 / TSC fixes
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;95f8bbc fix(deploy): make ecosystem.config.js host-portable&lt;/code&gt; — removed hardcoded absolute paths so the same config works on dev and prod. PM2 now boots the same from any &lt;code&gt;$HOME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;6ebab0d fix(ui): drop unused generatingCount state to unblock tsc build&lt;/code&gt; — dead state variable tripped the TypeScript build after a recent cleanup. Deleted and the build passed.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="commit-log"&gt;Commit log
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Message&lt;/th&gt;
 &lt;th&gt;Area&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(deploy): make ecosystem.config.js host-portable&lt;/td&gt;
 &lt;td&gt;PM2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(storage): build ref key cache from S3, not local filesystem&lt;/td&gt;
 &lt;td&gt;Storage&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(gen): attribute-aware model auto-injection&lt;/td&gt;
 &lt;td&gt;Generation logic&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(ui): drop unused generatingCount state to unblock tsc build&lt;/td&gt;
 &lt;td&gt;Frontend&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;chore(labels): re-label personas with 3-shot prompt and age estimates&lt;/td&gt;
 &lt;td&gt;Labeling&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Two patterns worth locking in. First, &amp;ldquo;build cache from the source of truth&amp;rdquo; beats &amp;ldquo;sync cache with source of truth&amp;rdquo; every time. The ref-key cache was fragile as long as it started from local state and hoped to reconcile with S3 later; building directly from S3 removes a whole category of drift bugs. Second, the clock-skew fix is a reminder that production OAuth failures are almost always distributed-systems issues (clock sync, DNS propagation, key rotation) rather than crypto issues — a 1-line fix after 10 minutes of log reading, which is exactly how it should feel in a mature stack.&lt;/p&gt;</description></item></channel></rss>