Spec2PR: Reimagining the SDLC for Intelligent Software Delivery

Spec2PR: Reimagining the SDLC for Intelligent Software Delivery

The future of software delivery is not faster coding. It is intelligent orchestration.

I didn’t set outto rethink the software delivery lifecycle.

I set out to help engineers ship faster. What I discovered along the way changed how I think about AI, engineering systems, and what the real bottlenecks in software delivery actually are.

This is the story of that journey — and the thinking behind Spec2PR.

The original goal: AI as a coding accelerator for teams

Like most engineering leaders who started exploring AI tools in the last few years, my initial hypothesis was straightforward: give engineers AI-assisted coding capabilities and they will move faster.

The goals were pragmatic:

  • Accelerate delivery velocity
  • Help junior engineers ramp up faster
  • Reduce onboarding friction
  • Improve implementation consistency
  • Give teams access to AI accelerators without requiring each engineer to become a prompt expert

This seemed reasonable. AI models were getting better rapidly. Code generation quality was improving. The value proposition appeared obvious.

So we built Spec2PR as an AI-assisted platform — an internal accelerator that would help engineering teams generate code faster and get more done with less effort.

For a while, it worked. And then the problems started.

The first realization: AI output drifts without engineering structure

The first cracks appeared gradually, then suddenly.

Without clear guidance, AI-generated code started drifting away from our company standards. Not in obvious, breaking ways — but in subtle, cumulative ways that created real engineering debt.

We observed:

  • Inconsistent implementations of similar patterns across teams
  • Architectural drift away from established platform standards
  • Governance gaps — AI-generated code that worked locally but violated organizational constraints
  • A dangerous dependency on individual prompt quality
  • Increased rework as teams discovered the drift during reviews

The frustrating part was that the models were not getting worse. The code was often syntactically correct and locally functional. The problem was not the AI’s intelligence.

The problem was the missing engineering structure around it.

“Prompt engineering was becoming accidental architecture.”

Each engineer was making micro-decisions about how to prompt the AI. Those micro-decisions accumulated into macro-inconsistencies. The platform was fast, but it was generating inconsistency at scale.

This was the first real lesson: AI tools amplify whatever context they are given. Without engineering structure, they amplify inconsistency.

Structuring engineering conversations with the RTCFR framework

The response to this problem was not to constrain the AI. It was to structure the conversation.

We introduced the RTCFR framework — Role, Task, Context, Format, Report — a structured approach to encoding the right engineering information into every implementation workflow before a single line of code is generated.

The difference is easier to see than describe. An unstructured prompt looks like:

“Build a REST API endpoint for user authentication.”

An RTCFR-structured workflow looks like:

“You are a senior backend engineer on a Java Spring Boot platform (Role). Implement a JWT authentication endpoint (Task). The service must meet our internal security standards, integrate with our existing OAuth provider, handle 10k RPS, and emit structured logs to our observability stack (Context). Output production-ready code with unit tests following our naming conventions (Format). Flag any assumptions about the security model (Report).”

Same request. Completely different output quality.

The goals were clear:

  • Structure engineering conversations before code generation begins
  • Embed engineering rigor into the workflow itself, not as a manual checklist after the fact
  • Abstract complexity away from junior engineers so they benefit from senior engineering thinking by default

The results were meaningful. Consistency improved. Drift reduced. Junior engineers were producing outputs that reflected organizational standards they had not yet internalized on their own.

But something more important had shifted conceptually.

The platform was no longer just generating code. It was operationalizing engineering thinking.

The AI had become a delivery layer for structured engineering intent — encoding how senior engineers think about problems and making that thinking available at every implementation step.

“Locally correct code can still create globally inconsistent systems.”

Fixing prompt inconsistency was valuable. But it had revealed something deeper.

The next bottleneck: upstream context quality determines downstream implementation quality

With implementation workflows now structured, I expected the quality problems to largely disappear.

They did not.

A new pattern emerged. Even when engineers followed the RTCFR framework correctly, the quality of the output was still constrained by the quality of the inputs coming from upstream.

Low-level design documents produced by architects frequently arrived without:

  • Non-functional requirements (NFRs)
  • Scalability considerations
  • Observability and monitoring requirements
  • Reliability and fault tolerance expectations
  • Operational runbook context
  • Security considerations
  • The broader “-ilities” that experienced engineers know to ask about

The AI faithfully implemented what the LLD described. And the LLD was incomplete.

“The further upstream I moved in the SDLC, the more I realized code generation was never the real problem.”

This was a significant realization. We had been optimizing a downstream symptom. The root cause was upstream context quality.

If the engineering intent captured at the design stage was incomplete, no amount of implementation optimization would fully compensate. The system was propagating incomplete intent with high efficiency.

Context degradation across the SDLC — how intent gets lost at every handoff

Stepping back and looking at the full delivery lifecycle, a clear pattern emerged.

Engineering intent degrades at every handoff across the SDLC:

  • Product intent is captured with clarity in strategy sessions, then diluted into vague user stories
  • User stories are handed to architects, who produce LLDs that may capture the functional requirement but lose the operational and NFR context
  • LLDs reach implementation teams, stripped of the architectural reasoning and tradeoff decisions that informed them
  • Implementations reach operations, where the teams responsible for running the system encounter its operational realities for the first time

By the time code reaches production, much of the original engineering intent has been lost. What remains is a functional implementation that may technically meet the specification while failing to reflect the full engineering thinking that went into the design.

“Software delivery problems are coordination problems, not coding problems.”

This is the systems-thinking insight at the core of Spec2PR’s evolution: the bottleneck was never code generation speed. The bottleneck was intent fidelity — the ability to preserve engineering thinking accurately as it moves through the delivery system.

AI coding tools address a real problem. But they address it at the wrong layer.

Intelligent SDLC orchestration: the core thesis and what comes next

This is where Spec2PR evolved from an AI coding accelerator into something more fundamental.

The platform began evolving toward a different goal: preserving and propagating engineering intent across the entire SDLC, from requirements through architecture, implementation, operations, and feedback.

The thesis crystallized into what I now call Intelligent Software Delivery:

Intelligent Software Delivery treats software engineering as a continuous, context-aware orchestration problem — not a sequence of isolated tasks.

Where today’s AI tools focus on local optimization, Intelligent SDLC Orchestration operates at the system level:

Today’s AI Tools Intelligent SDLC Orchestration
IDE-centric SDLC-centric
Stateless prompts Persistent engineering context
Local code optimization System-wide delivery optimization
Reactive assistance Proactive orchestration
Code generation Intent preservation
Developer productivity Delivery intelligence

Code generation is still part of this system. But it becomes one capability within a much larger engineering coordination layer — not the goal itself.

AI becomes transformative when it understands engineering systems, not just source files.

Closing: what I learned by going upstream

I started this journey trying to make engineers faster. What I found was a different problem entirely.

The teams that struggled most with AI-assisted development were not struggling because the models were insufficient. They were struggling because their delivery systems lacked the structure to give AI the context it needed to be useful at scale.

AI models are context amplifiers. The quality of what comes out is bounded by the quality of what goes in. And the quality of what goes in is an organizational problem, not a tooling problem.

“I started by trying to accelerate coding. I ended up realizing the real bottleneck was coordination.”

The future of software delivery is not faster coding. It is intelligent orchestration.

That is the idea behind Spec2PR — and the thread I will continue pulling on in this series.

This article is part of the **Spec2PR* series on Intelligent Software Delivery.*
DevEx AI Assistant — AI-powered SDLC acceleration for engineering teams.

Stop Naming Your Healthcare Columns Wrong — ISO-11179 Explained

If you’ve ever inherited a healthcare database with columns named DOB, PatientID, or CLAIM_NUMBER — this guide is for you.

Healthcare data engineering has a naming problem. Every team, every vendor, every health plan names their columns differently. A “member ID” becomes MemberID in one system, mem_id in another, PATIENT_KEY in a third, and mbr_identifier in a fourth.

When you try to join these systems — and you always have to join them — you spend more time figuring out what columns mean than actually building the pipeline.

There’s a standard that solves this. Most healthcare data engineers have never heard of it.

What is ISO-11179?

ISO-11179 is an international standard for naming data elements. It was developed specifically to make data interoperable across systems — which is exactly what healthcare data engineering needs.

The core idea is simple: every column name should follow a predictable structure:

So instead of DOB you get mbr_birth_dt.
Instead of CLAIM_NUMBER you get clm_nbr.
Instead of PatientID you get pt_id.

Every name tells you exactly what it is, what entity it belongs to, and what type of value to expect — without opening a data dictionary.

You can look up any of these standard abbreviations in the mdatool healthcare data glossary — it has 100,000+ standardized healthcare terms with their ISO-11179 abbreviations.

The Rules

1. Lowercase snake_case only

-- Wrong
MemberBirthDate
MEMBER_BIRTH_DATE
memberBirthDate

-- Right
mbr_birth_dt

Snowflake, BigQuery, and Databricks all handle lowercase snake_case consistently. Mixed case causes headaches with case-sensitive databases and makes joins fragile.

2. Standard abbreviations not made-up acronyms

This is where most teams go wrong. They abbreviate inconsistently:

-- Same concept, 4 different names on 4 tables
dob
birth_dt
patient_birth_date
DOB_DT

ISO-11179 gives you a fixed vocabulary. Learn it once, use it everywhere. The mdatool naming auditor checks your column names against this standard automatically.

3. Standard suffixes tell you the data type

This is the most powerful part of the standard. The suffix tells you exactly what kind of value to expect:

Suffix Meaning Example
_dt Date clm_pd_dt
_ts Timestamp load_ts
_cd Code diag_cd
_id Identifier mbr_id
_nbr Number clm_nbr
_amt Amount (money) clm_pd_amt
_nm Name prvdr_nm
_flg Boolean flag is_pd_flg
_cnt Count clm_line_cnt
_pct Percentage coin_pct
_txt Free text note_txt
_ind Indicator actv_ind

When you see clm_pd_amt you immediately know:

  • clm = claim
  • pd = paid
  • amt = monetary amount (NUMBER type, probably NUMBER(12,2))

No documentation required.

Standard Prefixes by Healthcare Domain

Claims

clm_id          -- claim identifier
clm_nbr         -- claim number (business key)
clm_pd_amt      -- claim paid amount
clm_alwd_amt    -- claim allowed amount
clm_chrg_amt    -- claim charge amount
clm_ded_amt     -- claim deductible amount
clm_coins_amt   -- claim coinsurance amount
clm_pd_dt       -- claim paid date
clm_rcvd_dt     -- claim received date
clm_sts_cd      -- claim status code
clm_typ_cd      -- claim type code

Browse all claims data terms →

Member / Enrollment

mbr_id          -- member identifier
mbr_nbr         -- member number
mbr_first_nm    -- member first name
mbr_last_nm     -- member last name
mbr_birth_dt    -- member birth date
mbr_gndr_cd     -- member gender code
mbr_eff_dt      -- member effective date
mbr_term_dt     -- member termination date
subscr_id       -- subscriber identifier
subscr_grp_nbr  -- subscriber group number

Browse all member enrollment terms →

Provider

prvdr_id        -- provider identifier
prvdr_npi       -- provider NPI number
prvdr_nm        -- provider name
prvdr_first_nm  -- provider first name
prvdr_last_nm   -- provider last name
prvdr_tax_cd    -- provider taxonomy code
prvdr_eff_dt    -- provider effective date
prvdr_term_dt   -- provider termination date

Browse all provider data terms →

Clinical

pt_id           -- patient identifier
diag_cd         -- diagnosis code
proc_cd         -- procedure code
enc_id          -- encounter identifier
admn_dt         -- admission date
dsch_dt         -- discharge date
icd_ver_cd      -- ICD version code

Browse all clinical data terms →

Pharmacy

rx_id           -- prescription identifier
ndc_cd          -- NDC drug code
rx_fill_dt      -- prescription fill date
days_sup_cnt    -- days supply count
qty_disp_cnt    -- quantity dispensed

Browse all pharmacy data terms →

The Most Common Naming Mistakes

Here’s what I see constantly in healthcare data warehouses — and the correct ISO-11179 equivalent:

Wrong Right Why
DOB mbr_birth_dt Ambiguous — whose DOB? What entity?
PatientID pt_id PascalCase, no domain prefix
CLAIM_NUMBER clm_nbr SCREAMING_SNAKE_CASE, wrong suffix
MemberEffectiveDate mbr_eff_dt PascalCase, too verbose
paid_amount clm_pd_amt Missing entity prefix
npi prvdr_npi No entity context — NPI of what?
status clm_sts_cd No entity, no suffix
flag is_pd_flg No entity, no context
date1 clm_eff_dt Completely meaningless
id mbr_id No entity prefix

Not sure what the right name is for a column? The mdatool name generator generates ISO-11179 standard names from a plain English description.

A Real Example — Before and After

Here’s a typical claims table I’ve encountered in the wild:

-- Before (nightmare to work with)
CREATE TABLE Claims (
  ID              INT,
  ClaimNum        VARCHAR(20),
  MemberID        VARCHAR(20),
  DOB             DATE,
  ProviderNPI     VARCHAR(10),
  DOS             DATE,
  BilledAmt       DECIMAL(12,2),
  PaidAmt         DECIMAL(12,2),
  AllowedAmt      DECIMAL(12,2),
  StatusCode      VARCHAR(3),
  PaidDate        DATE,
  ReceivedDate    DATE
);

And the same table following ISO-11179:

-- After (self-documenting)
CREATE TABLE fact_claim (
  clm_id          VARCHAR(20)      NOT NULL,
  clm_nbr         VARCHAR(20)      NOT NULL,
  mbr_id          VARCHAR(20)      NOT NULL,
  mbr_birth_dt    DATE,
  prvdr_npi       VARCHAR(10)      NOT NULL,
  svc_dt          DATE             NOT NULL,
  clm_chrg_amt    NUMBER(12,2),
  clm_pd_amt      NUMBER(12,2),
  clm_alwd_amt    NUMBER(12,2),
  clm_sts_cd      VARCHAR(3),
  clm_pd_dt       DATE,
  clm_rcvd_dt     DATE
);

The second version tells you everything you need to know without a single comment or data dictionary lookup. A new engineer joining your team can read this schema and immediately understand what each column represents.

Want to generate a schema like this automatically for any healthcare domain? Try the mdatool AI data modeling tool — it generates production-ready DDL with ISO-11179 column names for Snowflake, BigQuery, Databricks, and more.

Why This Matters Beyond Aesthetics

Consistent naming isn’t just clean code — it has real business impact in healthcare:

1. Faster onboarding
New data engineers can understand your schema in hours instead of weeks. In healthcare IT where turnover is high this matters enormously.

2. HEDIS and CMS reporting accuracy
When your column names clearly indicate what a field contains you’re less likely to join on the wrong field or use the wrong date in a quality measure denominator. Wrong HEDIS rates have real financial consequences.

3. Easier compliance audits
HIPAA auditors reviewing your data lineage want to understand what data flows where. A self-documenting schema makes this dramatically easier.

4. Multi-system integration
Healthcare data comes from Epic, Cerner, Availity, Change Healthcare, and dozens of other systems. When every team follows the same naming standard integration becomes mapping instead of archaeology.

How to Audit Your Existing Schema

If you have an existing schema that doesn’t follow these conventions here’s a SQL query to find the worst offenders:

-- Find columns that likely violate naming standards
SELECT 
  table_name,
  column_name,
  data_type,
  CASE
    WHEN column_name != LOWER(column_name) 
      THEN 'Has uppercase letters'
    WHEN column_name NOT LIKE '%_%' 
      THEN 'No underscore separator'
    WHEN column_name IN (
      'id','date','name','status',
      'code','flag','amount','number'
    )
      THEN 'Too generic — no entity prefix'
    WHEN LENGTH(column_name) < 4 
      THEN 'Too short — likely an abbreviation'
    ELSE 'Check manually'
  END as issue
FROM information_schema.columns
WHERE table_schema = 'your_schema'
  AND (
    column_name != LOWER(column_name)
    OR column_name NOT LIKE '%_%'
    OR LENGTH(column_name) < 4
  )
ORDER BY table_name, column_name;

Or skip the manual work and paste your DDL into the mdatool naming auditor — it checks against ISO-11179 healthcare standards automatically and flags every violation with a suggested fix.

Start Small

You don’t have to rename everything at once. Start with:

  1. All new tables follow ISO-11179 from day one
  2. Any table you’re modifying — rename columns in that PR
  3. Create views with standard names over legacy tables

Six months from now your new engineers will thank you. Your HEDIS submissions will be cleaner. Your CMS audits will be smoother.

And you’ll never spend 20 minutes figuring out whether DOS means “Date of Service” or “Denial of Service” again.

Resources mentioned in this article:

mdatool is a free platform for healthcare data engineers. No account required for most tools.

Evaluation: Prove it before you ship it

There’s a saying in AI circles: monitoring tells you what’s happening — evaluation tells you how good it is. You can have an agent that responds instantly, never crashes, and answers every question with absolute confidence. But confidence without correctness is just a well-dressed mistake.

That’s exactly why Oracle AI Agent Studio gives us two complementary capabilities: Monitoring and Evaluation. In this post, we’re focusing on Evaluation — what it is, how to set it up, and why you should care before your agent goes anywhere near production.

Curious about Monitoring? Check out this post.

Why Evaluation Matters

Evaluation ensures your agent can meet defined standards and business outcomes across three dimensions: accuracy, latency, and token usage.

Without it, you’re essentially deploying on vibes.

With a proper evaluation framework, you can validate that your agent answers correctly, responds within acceptable time, and doesn’t burn through your token budget faster than your cloud spend budget. It’s the closest thing to a test suite for your AI agent.

The Metrics: What Gets Measured

Oracle AI Agent Studio provides a rich set of metrics, and it’s worth understanding which ones are available for Evaluation versus Monitoring, because they serve different purposes.

Metric Evaluation Monitoring
Error Rate ✅ ✅
Error Count ✅ ✅
Session Count ✅ ✅
P99 Latency ✅ ✅
P50 Latency ✅ ✅
Total Tokens ✅ ✅
Input Token Count ❌ ✅
Output Token Count ✅ ✅
Median Correctness ✅ ❌
Groundedness ✅ ❌
Answer Relevance ✅ ❌
Context Relevance ✅ ❌

The quality metrics — Correctness, Groundedness, Answer Relevance, and Context Relevance — are exclusive to Evaluation. These are the metrics that tell you whether your agent is genuinely useful, not just technically operational.

A quick breakdown of what these mean in practice:

Median Correctness measures how closely your agent’s answer matches the expected reference response. Scores range from 0 to 1. Think of it as your agent’s grade on the test.

Groundedness measures whether the generated answer is actually grounded in the retrieved source content. A grounded response stays faithful to what the knowledge base says — it doesn’t hallucinate or embellish. (Hallucination: the nemesis of every enterprise AI implementation.)

Answer Relevance measures how directly and precisely the agent’s response addresses the user’s question. Getting the right answer to the wrong question doesn’t count.

Context Relevance measures the quality of the retrieved information itself — whether the context pulled in by the agent was actually appropriate and reliable enough to produce a good answer.

Setting Up an Evaluation: Step by Step

Step 1 — Define Your Evaluation Set

Before you can run anything, you need to define an evaluation set. Think of this as your test plan. It includes:

  • Test questions — the inputs your agent needs to handle
  • Expected responses — the gold-standard answers you’re measuring against
  • Success criteria — the thresholds each metric needs to meet

An evaluation set without expected responses is just a demo. The expected responses are what turn a run into a meaningful quality gate.

Step 2 — Choose Your Run Mode

When setting up your evaluation run, you’ll choose between two modes:

Sequential runs questions in the exact order you define them. Use this when one question depends on the context from the previous one — for example, a multi-turn conversation flow.

Random runs questions in a randomised order. This is useful when testing independent questions where order doesn’t matter, and it helps reduce positional bias in your evaluation results.

Step 3 — Define Your Questions

In the Questions tab, add the questions users are expected to ask your agent, paired with the exact responses you want the agent to return.

Here’s an example from an HR benefits agent:

Q: Who is eligible for the benefits program?

A: Eligibility Criteria:

  1. Full-time employees working 30+ hours per week are eligible for full benefits.
  2. Part-time employees may qualify for limited benefits.
  3. Benefits eligibility begins on the first day of the month following hire date.

Dependents (spouse and children under age 26) may be enrolled in applicable plans.

Keep your expected responses as close to production-quality as possible. The correctness metric is only as good as the reference answer you define.

Step 4 — Configure Your Metrics

In the Metrics tab, you select which metrics to include in this evaluation run. This is where you tailor the evaluation to your agent’s specific use case and business requirements.

For example:

  • If your agent doesn’t invoke any APIs, you can exclude API error metrics — no point cluttering your results with noise.
  • If accuracy is your top priority (say, a policy or compliance agent), set your correctness threshold high — 0.8 is a reasonable baseline for enterprise use.
  • If token cost is a concern, configure output token thresholds to flag responses that are running unnecessarily long.

Metrics without thresholds are just numbers. Thresholds are what turn numbers into pass/fail signals.

Step 5 — Initiate the Evaluation Run

Click Initiate Evaluation Run. Oracle AI Agent Studio will execute the evaluation and return results for each question, including:

  • Actual response vs. expected response — side by side
  • Latency per question
  • Token usage (input and output)
  • Quality scores for the metrics you selected

Reading Your Results

After the run completes, reviewing the results is where the real value surfaces. Here’s an example of what you might find:

Latency: One question took over 20 seconds — exceeding the defined threshold. That’s a red flag worth investigating. It could point to an overly complex retrieval step, a large system prompt, or a knowledge base that needs optimisation. The remaining questions came in well within threshold.

Token Usage: Both input and output token counts were within acceptable limits. Good news for the budget.

Correctness: With a threshold of 0.8, any question scoring below that benchmark gets flagged for review. Patterns in low-scoring questions often reveal gaps in your knowledge base or ambiguities in your system prompt.

This combination of latency, cost, and quality signals gives you a complete picture — not just “did it answer?” but “did it answer well, quickly, and efficiently?”

Final Thoughts

An AI agent that passes evaluation isn’t just technically sound — it’s one you can actually stand behind when a business user asks, “How do we know this is right?”

Defining quality thresholds, building meaningful evaluation sets, and reviewing results against expected outcomes is what separates a production-ready agent from a prototype running in a demo environment. Oracle AI Agent Studio gives you the tooling to do this properly. Use it.

I’ve Given Up on Bun. I’m Removing It from SuperRails

GitHub logo

Bun’s implementation language has been migrated from Zig to Rust.

I have no intention of criticizing either Zig or Rust. I think both are excellent languages.
What I want to criticize is Bun’s development process.




Rewrite Bun in Rust

#30412

Blog post with details coming soon.

It passes Bun’s pre-existing test suite on all platforms (and fixes several memory leaks and flaky tests), the binary size shrinks by 3 MB – 8 MB, the benchmarks are between neutral and faster – and most importantly, we now have compiler-assisted tools for catching & preventing memory bugs, which have costed the team an enormous amount of development & debugging time over the years.

The codebase is otherwise largely the same. The same architecture, the same data structures. Bun still uses few 3rd party libraries. No async rust.

To try this, run:

bun upgrade --canary

Please do file issues if you run into any. If this thread gets crazy I will lock it.

Note:

  • Still some optimization work to do before this lands in non-canary version.
  • Still some cleanup work to do (which will come in a series of follow-up PRs)

github pr rows

I have never seen a diff this large before.
What is even more surprising is that, using Claude Code, they reportedly went from implementation to review and merge in less than a week.

Anthropic, which owns Bun’s developer Oven, will likely use this “success story” to further market Claude models and tools.

Even if the migration to Rust has solved the memory leak issues, and even if AI implemented excellent code at incredible speed, I am dismayed by a development process that can release code almost instantly without much review.

To be clear, I am not claiming that AI-generated code is evil. AI can produce useful code, and I also rely on vibe coding in areas where the security risk is low.

But I still review the generated code again and again before I trust it.

My criticism is directed at the act of releasing code without taking full responsibility for it — that is, shipping a massive change without carefully reading and understanding every part of the diff.

Given the sheer size of this change, I do not believe it was physically possible for the Bun development team to thoroughly review all of it in less than a week. That, not the use of AI itself, is what I find unacceptable.

Some people say, “This is the first step in a gradual experiment. It will get better from here, and the Rust code will become more refined as unsafe code is eliminated.”
Don’t be ridiculous. My products—SuperRails and LazyCafe —use Bun. Are my products Anthropic’s sandbox?

In the near future, I plan to migrate SuperRails from Bun to a pnpm-based esbuild setup. I will consxider similar changes for other products, including LazyCafe.

Anthropic and the Bun development team are so focused on short-term problems—how to go public as quickly as possible, and how to resolve memory leak issues as quickly as possible—that they lack the perspective of keeping these tools and products in society for the long term.

This way of doing things does not align with my development philosophy: quietly running businesses that can survive for decades.


Super Rails | Rails Boilerplate for Solopreneurs

A production-ready Rails boilerplate with Devise, Avo, Stripe, ViewComponent and more. Skip the setup, ship your MVP in days.

favicon
super-rails.com

I Built a Wireless Phone Camera Monitor With Remote Controls

You know that thing where you set up your laptop camera, hit record, walk to your spot, do the thing, walk back to check, and realize you were out of frame the whole time?

I did that five times in one session.

My phone was sitting right there. Better camera. I can put it anywhere. But every solution I found wanted me to download an app, install a driver, or pay for HD. For something that should just work.

So I spent a weekend building Camcastar.

What it does
Open camcastar.com on your laptop. A QR code appears. Scan it with your phone. Your phone camera shows up live on your laptop screen.

That’s it.

From your laptop you can control everything remotely:

  • Switch between front and rear camera
  • Adjust zoom
  • Toggle the flashlight
  • Control Resolution and FPS
  • Start/Stop Recording from laptop
  • No app. No cables. No account. Free

Who it’s for
Anyone who has ever needed a better camera angle without the setup headache. Creators, developers on video calls, people recording tutorials, anyone who wants to see what their phone sees from their laptop screen.

Try it
camcastar.com

Both devices need to be on the same Wi-Fi. Open the laptop page, scan the QR code with your phone camera, allow camera access, and you are live in about 10 seconds.

Would love honest feedback. What works, what doesn’t, what you’d want next. Drop a comment or use the feedback form at contact.

Why “Waterproof” Hardware Needs More Than a Marketing Claim


When people see the word “waterproof” on a product, they often think it means one simple thing: water cannot get inside.

But for engineers, product designers, and testing labs, waterproofing is not that simple.

A device may survive light rain but fail under water jets.
It may handle splashing water but fail after immersion.
It may pass one manual spray test but fail when the same condition is repeated in a controlled laboratory setup.

That is why waterproof performance needs to be tested, not guessed.

Waterproofing is about conditions

In hardware testing, the question is not just:

  • “Can this product resist water?”

The better question is:

“Under what water condition can this product resist water?”

That difference matters.

A smart watch, an outdoor lamp, a bathroom appliance, an electrical enclosure, and an EV charging component may all be described as “water-resistant” or “waterproof,” but the real exposure conditions are very different.

Some products face dripping water.
Some face rain and splashing.
Some face strong water jets.
Some may be temporarily immersed in water.

Each situation creates different risks for the product design.

What IPX testing actually helps verify

IPX waterproof testing is used to evaluate how well an enclosure protects internal parts from water ingress.

In simple terms:

  • IPX1 / IPX2: dripping water
  • IPX3 / IPX4: rain and splashing water
  • IPX5 / IPX6: water jets
  • IPX7 / IPX8: immersion under specified conditions

For developers and hardware teams, these levels are useful because they turn a vague claim like “waterproof” into a defined test condition,That makes product validation more repeatable.

Why repeatability matters

A manual spray test may look convincing, but it is not enough for serious product validation.

For a waterproof test to be meaningful, several parameters need to be controlled:

  • water flow rate
  • spray angle
  • spray distance
  • water pressure
  • test duration
  • sample position
  • immersion depth
  • test consistency between batches

If these conditions are not controlled, two tests that look similar may actually produce different results.

That is a problem for R&D, quality control, certification testing, and customer reliability.

Real-life impact

This is not only a laboratory issue.

For ordinary users, poor waterproof performance can lead to corrosion, short circuits, insulation failure, product shutdown, or even safety risks.

For manufacturers, it can lead to warranty claims, product recalls, failed certification, and damage to brand reputation.

That is why waterproof testing is part of responsible hardware development.


Example: how labs handle multiple IPX test levels

For laboratories that need to evaluate different waterproof levels, the challenge is often not only the test itself, but how to keep each test condition consistent.

Dripping water, rain spray, water jets, and immersion are very different test environments. If each test is done with a separate temporary setup, it becomes harder to maintain the same level of control, documentation, and repeatability.

This is why some labs use integrated IPX1 to IPX8 waterproof test systems. These systems are designed to organize multiple waterproof test methods within one platform, making it easier to manage test parameters such as flow rate, spray angle, pressure, duration, and immersion depth.

One example is KingPo’s IPX1 to IPX8 Waterproof Test Chamber, which is built for controlled waterproof testing across IPX1 to IPX8 levels, including dripping water, spraying/splashing, water jet, and immersion tests.

For one example of an integrated system, see this IPX1 to IPX8 waterproof test chamber.

Final thought

  • Waterproof should not just be a word printed on a product page.For hardware products, it should be connected to a specific test level, a defined test condition, and a repeatable result.That is what makes an IP rating meaningful.

RAG Series (19): Incremental Updates — Keeping the Knowledge Base Fresh

Knowledge Bases Are Not Static

Every article in this series so far has shared one implicit assumption: documents are loaded once, and the index never changes.

Production doesn’t work like that.

Product documentation updates weekly. Knowledge base articles are added daily. Outdated content gets retired. Every time something changes, you face a choice:

Option A: Full rebuild

Re-embed every document — including the ones that didn’t change — and rebuild the entire vector index from scratch. Simple to implement. Expensive to run:

  • You pay for embedding every document every time
  • 1000 documents, 5 changed: still 1000 embed calls
  • Rebuild time grows proportionally to corpus size, not change size

Option B: Incremental update

Store a content hash for each indexed document. On the next sync, only process the documents whose hash changed — embed the new ones, replace the modified ones, clean up the deleted ones, skip everything else.

LangChain’s Indexing API implements Option B.

How the Indexing API Works

Two components:

SQLRecordManager: A SQLite database that stores a record for each indexed document:

source         |  content_hash           |  indexed_at
rag-intro      |  a3f8b2c1...            |  2026-05-15 10:00
ragas          |  d9e2f1a4...            |  2026-05-15 10:00
vector-db      |  7c4b8e3f...            |  2026-05-15 10:00
...

index() function: Compares the current document batch against the RecordManager and decides what happens to each document:

For each document in the batch:
  Hash matches   → skip (num_skipped++)
  Hash differs   → delete old version, insert new (num_deleted++, num_added++)
  First time     → insert (num_added++)

After processing all documents (cleanup="full"):
  In RecordManager but not in batch → delete (num_deleted++)

cleanup="full" handles the deletion case. Without it, documents that were removed from your knowledge base continue to live in the vector store and show up in retrieval results — stale content, indefinitely.

Core Implementation

RecordManager Setup

from langchain_classic.indexes import SQLRecordManager, index

NAMESPACE = "chroma/rag_knowledge_base"

record_manager = SQLRecordManager(
    NAMESPACE,
    db_url="sqlite:///record_manager.db",
)
record_manager.create_schema()   # create tables on first run

NAMESPACE acts as a partition key. One SQLite file can manage multiple independent knowledge bases without interference.

The Sync Function

def sync_knowledge_base(docs: list[Document]) -> dict:
    """Incrementally sync a document batch into the vector store.

    - Unchanged documents: skipped (no embedding API call)
    - New / modified documents: embedded and written
    - Removed documents: deleted from the vector store
    """
    return index(
        docs,
        record_manager,
        vectorstore,
        cleanup="full",          # auto-remove docs not in this batch
        source_id_key="source",  # metadata["source"] identifies each document
    )

source_id_key is the document identity key. Two documents with the same source are treated as different versions of the same document. If content changes, the old version is deleted and the new version is added.

Documents Must Have a source Field

Document(
    page_content="...",
    metadata={"source": "rag-intro"},   # required for version tracking
)

Documents without a source can’t be tracked incrementally — they’ll be treated as new every single time.

Experiment: Three Sync Rounds

Dataset Design

V1 (initial knowledge base, 6 documents):
rag-intro, ragas, vector-db, embedding, rerank, chunking

V2 (simulated update cycle):

Change Type Document Description
Unchanged rag-intro, vector-db, rerank Identical content
Modified ragas Added faithfulness explanation
Modified chunking Added contextual retrieval section
Deleted embedding Not present in V2 batch
Added advanced-rag New document
Added conv-rag New document

V1 → V2: 3 unchanged, 2 modified, 1 deleted, 2 added.

Results

======================================================================
  Scenario 1: Initial Index (V1 — 6 documents)
======================================================================

  [Initial Index]
  ┌─────────────────────────────────────────┐
  │  added:       6  (newly embedded)       │
  │  skipped:     0  (content unchanged)    │
  │  deleted:     0  (removed/replaced)     │
  ├─────────────────────────────────────────┤
  │  embed calls:    6                      │
  │  wall time:   0.913s                    │
  └─────────────────────────────────────────┘

======================================================================
  Scenario 2: Incremental Update (V2)
======================================================================

  [Incremental Update]
  ┌─────────────────────────────────────────┐
  │  added:       4  (newly embedded)       │
  │  skipped:     3  (content unchanged)    │
  │  deleted:     3  (removed/replaced)     │
  ├─────────────────────────────────────────┤
  │  embed calls:    4                      │
  │  wall time:   0.891s                    │
  └─────────────────────────────────────────┘

======================================================================
  Scenario 3: Full Rebuild (V2, record manager wiped)
======================================================================

  [Full Rebuild]
  ┌─────────────────────────────────────────┐
  │  added:       7  (newly embedded)       │
  │  skipped:     0  (content unchanged)    │
  │  deleted:     0  (removed/replaced)     │
  ├─────────────────────────────────────────┤
  │  embed calls:    7                      │
  │  wall time:   0.494s                    │
  └─────────────────────────────────────────┘

Cost Comparison

  ┌──────────────────────┬───────────────┬───────────────┐
  │                      │   Incremental │  Full Rebuild │
  ├──────────────────────┼───────────────┼───────────────┤
  │  Documents embedded  │       4       │       7       │
  │  Documents skipped   │       3       │       0       │
  │  Embedding savings   │    42.9%      │     0%        │
  └──────────────────────┴───────────────┴───────────────┘

Incremental update triggered 4 embed calls; full rebuild triggered 7. That’s 42.9% fewer API calls for the same end state.

An Honest Look at the Timing Results

Full rebuild was actually faster (0.494s vs 0.891s). This deserves an explanation.

With 7 small documents, the SQLite hash lookup and comparison overhead costs more than the time saved by skipping 3 embed calls. Embedding calls go out as batched async HTTP requests — latency is dominated by network round-trip. SQLite operations are synchronous local disk I/O. At small scale, the bookkeeping costs more than the savings.

This reverses quickly at realistic scale:

Scenario: 1,000-document knowledge base, 5% daily change rate (50 docs)

Full rebuild:    1,000 embed calls per day
Incremental:        50 embed calls per day  →  95% reduction

At $0.0001 per embed call (typical for bge-large-zh-v1.5):
  Full rebuild:   ~$100/day (assuming avg 200 tokens/doc)
  Incremental:    ~$5/day

At 10,000 documents:
  Full rebuild:   ~$1,000/day
  Incremental:    ~$50/day

The time savings at small scale are not meaningful. The API cost savings are real from day one, and both metrics grow with corpus size.

Two Kinds of Deletion

deleted: 3 in the incremental result contains two different things:

  1. Replacement deletion (2): ragas and chunking changed content. The old versions are deleted from the vector store; new versions are embedded and inserted. Net document count for these sources: unchanged.

  2. Cleanup deletion (1): the embedding document was not in the V2 batch at all. With cleanup="full", after processing all documents in the batch, the indexer checks the RecordManager for any source not seen in this run — finds embedding, and removes it.

If you used cleanup=None instead:

# Not recommended: stale documents accumulate
index(docs, record_manager, vectorstore, cleanup=None)

# Recommended: full sync, stale content auto-removed
index(docs, record_manager, vectorstore, cleanup="full", source_id_key="source")

Without cleanup, embedding stays in the vector store indefinitely. Users querying about embedding models would still get answers based on the retired document. This is the “ghost document” problem — one of the more insidious production bugs in RAG systems, because it’s invisible until someone notices the answers reference content that no longer exists.

Production Integration Pattern

In practice, incremental updates are triggered by a scheduled job or a document change event:

import glob

def load_documents_from_dir(docs_dir: str) -> list[Document]:
    """Load documents from the filesystem, using file path as source."""
    docs = []
    for filepath in glob.glob(f"{docs_dir}/**/*.md", recursive=True):
        with open(filepath, encoding="utf-8") as f:
            content = f.read()
        docs.append(Document(
            page_content=content,
            metadata={"source": filepath},
        ))
    return docs

# Scheduled job: sync every hour
def hourly_sync():
    docs = load_documents_from_dir("./knowledge_base")
    result = index(
        docs,
        record_manager,
        vectorstore,
        cleanup="full",
        source_id_key="source",
    )
    print(
        f"Sync done: +{result['num_added']} added  "
        f"~{result['num_deleted']} deleted  "
        f"={result['num_skipped']} skipped"
    )

File path as source is naturally unique. File content changes automatically invalidate the stored hash, triggering re-embedding on the next sync. No extra tracking code needed.

RecordManager Persistence

SQLRecordManager persists to disk, so the hash registry survives service restarts. For production:

# Development / single machine
record_manager = SQLRecordManager(
    "namespace",
    db_url="sqlite:///record_manager.db",
)

# Production / distributed (multiple service instances share one registry)
record_manager = SQLRecordManager(
    "namespace",
    db_url="postgresql://user:pass@host/dbname",
)

SQLite works for single-instance deployments. Switch to PostgreSQL when multiple instances need to share the same RecordManager — otherwise concurrent writes will corrupt the hash registry.

Full Code

Complete code is open-sourced at:

https://github.com/chendongqi/llm-in-action/tree/main/19-incremental-update

Key file:

  • incremental_update.py — three sync scenarios, counting wrapper, cost comparison, query verification

How to run:

git clone https://github.com/chendongqi/llm-in-action
cd 19-incremental-update
cp .env.example .env
pip install -r requirements.txt
python incremental_update.py

Summary

This article implemented incremental knowledge base updates using LangChain’s Indexing API. Key findings:

  1. Content hash tracking is the mechanism — RecordManager stores a hash for each document; unchanged → skip, modified → delete old + insert new, deleted → cleanup removes it
  2. 42.9% embedding reduction — 7 documents, 3 unchanged: only 4 embed calls instead of 7. The ratio improves as the corpus grows and change rate decreases
  3. Wall time savings don’t show at small scale — SQLite hash lookup overhead dominates at 7 documents; time savings become significant at 1,000+ documents
  4. cleanup="full" prevents ghost documents — without it, deleted documents stay in the vector store indefinitely and keep appearing in retrieval results

Incremental updates are the step that moves RAG from “demo that works” to “production system that stays correct.” A knowledge base is not a one-time artifact — it needs to evolve alongside the business that depends on it.

References

Three memory-leak patterns in long-running scrapers (and how I caught them after 968 Trustpilot runs)

Memory leaks in scrapers do not crash the run. They quietly bump the Apify Memory limit from 1 GB to 2 GB to 4 GB, double the per-run cost, and only get spotted weeks later on a compute-unit invoice.

After 968 Trustpilot runs (~80–300 review pages each, ~150k page hits cumulative), I started sampling RSS every 1,000 pages. The growth pattern told a different story than the logs. Below are the three patterns that account for ~90% of the leaks I have seen across my 32 published Apify actors.

1. The unbounded asyncio queue

The most common pattern. A producer coroutine fetches URLs faster than the consumer parses them, so the in-memory queue grows linearly with runtime.

# leaks at high concurrency
queue = asyncio.Queue()  # no maxsize
async def producer():
    async for url in source:
        await queue.put(url)  # never blocks

async def consumer():
    while True:
        url = await queue.get()
        await process(url)

If process() is slower than source (which is true for most JS-rendered sites), the queue accumulates. On a Trustpilot run that fetched a company with 12,000 reviews, the queue held ~9,500 URLs at peak — about 380 MB of bytestrings.

Fix:

queue = asyncio.Queue(maxsize=200)  # producer blocks at 200

A bounded queue forces the producer to wait. Memory stays flat; throughput drops by less than 5% because the consumer never sat idle anyway — the bottleneck was the network, not the queue.

2. Per-URL re.compile bypassing Python’s regex cache

CPython has a built-in regex cache at re._cache (default 512 entries). The naive assumption is that re.search(pattern, text) is cached and cheap on the second call.

It is — for string patterns. But the moment you build the pattern dynamically per URL, every call is a cache miss and a new compiled object:

# leaks at scale
def extract_review_id(html: str, slug: str):
    pat = rf"review-{slug}-(d+)"          # dynamic pattern
    m = re.search(pat, html)
    return m.group(1) if m else None

Each unique slug (company name) puts a new entry in re._cache. The cap is 512, so it does not “leak” forever — but the eviction is a clear-the-whole-dict operation under the hood, and the compiled pattern keeps an internal __weakref__ to anything it captured during compile (locale, flags). Across a long run with thousands of distinct slugs, RSS would climb 3–5 MB per 1,000 pages.

Fix: lift the dynamic part out and use a single static pattern with a group capture:

REVIEW_ID = re.compile(r"review-([a-z0-9-]+)-(d+)")

def extract_review_id(html: str, slug: str):
    for m in REVIEW_ID.finditer(html):
        if m.group(1) == slug:
            return m.group(2)
    return None

One compiled pattern, one cache slot. RSS curve flattens.

3. BeautifulSoup soup retention in long-lived lists

This one is sneaky. The code looks correct:

results = []
for url in urls:
    html = await fetch(url)
    soup = BeautifulSoup(html, "lxml")
    results.append({
        "title": soup.title.string,
        "body": soup.select_one("article").get_text(),
    })

The dictionaries look small — a few hundred bytes of text each. But soup.title.string and the result of select_one(...).get_text() are not plain Python strings. They are NavigableString and bs4.element.Tag proxies that hold a back-reference to the parent soup. As long as those proxies live in results, the entire parse tree (often 200 KB–2 MB per page) stays in memory.

After 2,500 review pages, my Trustpilot worker had ~3 GB RSS — almost all of it old soup trees being kept alive by .string references in results.

Fix: coerce to plain str at the point of extraction:

results.append({
    "title": str(soup.title.string) if soup.title else None,
    "body": str(soup.select_one("article").get_text()) if soup.select_one("article") else "",
})
# soup goes out of scope at end of loop → entire tree freed

str(...) copies the bytes into a fresh Python string with no back-reference. The soup is now garbage as soon as the loop iterates. RSS on the same 2,500-page run dropped from 3 GB to 410 MB.

Detection: RSS sampling every 1,000 pages

I added a 12-line probe to every scraper:

import os, psutil, time
_proc = psutil.Process(os.getpid())
_samples = []

def rss_tick(page_count: int):
    if page_count % 1000 == 0:
        rss_mb = _proc.memory_info().rss / 1024 / 1024
        _samples.append((page_count, rss_mb, time.time()))
        if len(_samples) >= 3:
            first, last = _samples[-3], _samples[-1]
            growth_per_1k = (last[1] - first[1]) / ((last[0] - first[0]) / 1000)
            if growth_per_1k > 50:  # >50 MB per 1,000 pages
                print(f"LEAK ALERT: +{growth_per_1k:.1f} MB/1k pages")

The threshold of 50 MB per 1,000 pages is conservative — anything above 20 MB on a steady-state run is worth investigating. The output gets piped to Apify’s dataset, so I can grep across runs.

The cost angle nobody mentions

Memory leaks rarely crash a scraper. What they do is force you to bump the actor’s Memory configuration:

  • 1 GB → 2 GB: doubles compute-unit consumption per second
  • 2 GB → 4 GB: quadruples it vs the 1 GB baseline

On Apify pricing, a 4 GB run at $0.0004/CU-second costs ~4× a properly-tuned 1 GB run for the same wall-clock time. Across 968 Trustpilot runs that would have been an extra ~$120/year for nothing — pure operational waste because nobody profiled RSS.

The 3 patterns above cover ~90% of leaks I have hit in production. Add the RSS probe to every long-running scraper, set the leak threshold at 50 MB/1k pages, and you will catch the next one in the first dev cycle instead of the next billing cycle.

More production scraping notes: t.me/scraping_ai. Originally published at blog.spinov.online.

Animated SVG Icons in React: Why SMIL Still Wins in 2026

title: “Animated SVG Icons in React: Why SMIL Still Wins in 2026”
published: true
tags: svg, react, animation, webdev

series: “Animated SVG Icons with CSS”

For years, the default answer for adding motion to web interfaces has been to reach for a JavaScript animation library. But what if you could achieve complex, performant, and accessible animations with zero runtime JavaScript? That’s the promise of SMIL (Synchronized Multimedia Integration Language) animations inside SVG, and it’s the core technology powering the cssvg-icons library for React and Next.js.

In 2026, as web bundles grow and performance budgets tighten, SMIL offers a radically simple and self-contained alternative. Here’s why this decades-old web standard isn’t just viable—it’s often the superior choice for icon animation.

The Zero-JS Advantage: Animation Without the Bundle Weight

The most compelling reason to use SMIL is its complete lack of JavaScript at runtime. The entire animation logic is embedded directly within the SVG markup.

Consider the Activity icon from the library. Its pulsing, scanning effect is defined by a series of <animate> and <animateTransform> elements inside the SVG. The browser’s native renderer executes this without a single line of JS.

// icons/activity/activity.tsx (excerpt)
<animateTransform
  attributeName="transform"
  type="translate"
  values="20,20;20,20;20,20"
  dur="4s"
  repeatCount="indefinite"
  calcMode="spline"
  keyTimes="0;0.0025;1"
  keySplines="0 0 1 1;0 0 1 1"
  additive="replace"
/>

This pattern means your React component is just a declarative wrapper. The heavy lifting is done by the browser’s SVG engine. For a typical icon library, this can shave hundreds of kilobytes off your initial JavaScript bundle. In a world of Core Web Vitals, that’s a significant win.

Declarative and Portable: Animation as Data

SMIL animations are pure data. They are not tied to any framework’s lifecycle or re-render cycles. This makes them incredibly portable.

The cssvg-icons library exports standard React components, but the animation logic is framework-agnostic. You could take the raw SVG from icons/crown/crown.svg and drop it into an Angular, Vue, or Svelte project with identical behavior. The animation is a property of the SVG itself, not the component wrapper.

This declarative nature also aligns perfectly with React’s philosophy. The component’s props (size, color, strokeWidth) control presentation, while the SVG’s internal SMIL tags control motion. There’s no useEffect hook to clean up, no requestAnimationFrame loop to manage. The animation state is isolated and self-maintaining.

Complex Motion Without the Code Complexity

A common misconception is that SMIL is only for simple fades and slides. The Menu icon proves otherwise. It uses multiple, staggered animations on three separate lines to create a smooth morphing effect from a hamburger menu to an “X”.

// icons/menu/menu.tsx (excerpt)
<g transform="translate(20,11.97) rotate(0,0,0) scale(0.2,0.2)">
  <animateTransform
    attributeName="transform"
    type="scale"
    values="0.2,0.2;0.15,0.15;0.2,0.2"
    dur="2s"
    repeatCount="indefinite"
    calcMode="spline"
    keyTimes="0;0.5;1"
    keySplines="0 0 1 1;0 0 1 1"
    additive="sum"
  />
  <line ... />
</g>

To achieve this with a JavaScript library, you’d need to calculate keyframes, manage timing offsets, and ensure the animation restarts cleanly on component mount. With SMIL, it’s a few lines of declarative markup. The browser handles the interpolation, timing functions, and lifecycle automatically.

Accessibility and the Shadow DOM

SVG icons are often used as decorative elements or interactive controls. SMIL animations, being part of the DOM, respect browser accessibility features in ways that canvas or WebGL-based animations often do not.

Because the animation is defined in the DOM, screen readers can still parse the SVG’s semantic structure (<title>, <desc>) even while it’s moving. The aria-hidden="true" attribute used in cssvg-icons components ensures icons are ignored by assistive tech when purely decorative, but for interactive icons, the underlying structure remains accessible.

Furthermore, SMIL animations are contained within the SVG’s coordinate system. They don’t leak into the global CSS namespace or require complex CSS class management to isolate styles. This containment is a natural fit for component-based architectures.

Performance and the Main Thread

JavaScript animations run on the main thread, competing for resources with your React app’s state updates, event handlers, and other logic. SMIL animations are typically handled by the browser’s compositor thread, which is optimized for graphics rendering.

This means smoother animations, especially on mobile devices, and less jank when your UI is under heavy interaction load. The Search icon’s scanning magnifier animation, for instance, runs independently of any JS-driven UI updates on the page.

// icons/search/search.tsx (excerpt)
<animateTransform
  attributeName="transform"
  type="translate"
  values="20,20;22.48,15.52;16.16,17.76;23.12,20.72;20,20"
  dur="4s"
  repeatCount="indefinite"
  calcMode="spline"
  keyTimes="0;0.2475;0.5;0.7525;1"
  keySplines="0 0 1 1;0 0 1 1;0 0 1 1;0 0 1 1"
  additive="replace"
/>

The browser can often offload SMIL animation frames to the GPU, resulting in 60fps motion with minimal CPU impact.

The Browser Support Reality in 2026

It’s true that SMIL support has been inconsistent historically. But in 2026, the landscape is different. All modern browsers (Chrome, Firefox, Safari, Edge) have mature, stable SVG implementations. SMIL is a W3C recommendation from 1999—it’s one of the most battle-tested web standards we have.

For the rare edge case (like an old version of Internet Explorer), the cssvg-icons components are designed to fall back gracefully. The SVG is still a valid, static icon. The animation is a progressive enhancement. This is a more robust strategy than depending on a third-party JS library that might fail to load or execute.

When Would You Not Use SMIL?

SMIL isn’t a silver bullet. For highly interactive, gesture-driven animations that respond to complex user physics, a JS library like Framer Motion might be necessary. SMIL is best for predefined, looping, or triggered animations—exactly the kind used in icon sets, loaders, and micro-interactions.

If your animation needs to change dynamically based on complex state (e.g., a graph that morphs based on live data), SMIL’s declarative nature can become a limitation. But for the vast majority of UI icons, it’s more than sufficient.

Getting Started with SMIL in Your React Project

The cssvg-icons library demonstrates the pattern:

  1. Create a simple React wrapper that accepts props for size, color, and strokeWidth.
  2. Embed the SVG with your SMIL animation tags directly inside the return statement.
  3. Use it like any other component:
import Bell from 'cssvg-icons/icons/bell';

export default function NotificationBell() {
  return (
    <div className="relative">
      <Bell size={24} color="red" />
      <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full">
        {unreadCount}
      </span>
    </div>
  );
}

The animation plays automatically on mount. No useEffect, no cleanup function, no bundle tax.

Conclusion

SMIL animations in SVG offer a path to lightweight, performant, and accessible motion that integrates seamlessly with React’s declarative model. In 2026, as we optimize for faster load times and smoother experiences, revisiting this mature web standard isn’t just nostalgic—it’s a practical engineering decision.

The cssvg-icons library exists to prove this point: you can have beautiful, complex, animated icons without the JavaScript overhead. It’s a testament to the power of web platform APIs done right.

Explore the full collection of animated SVG icons and see the SMIL patterns in action at https://icon.cssvg.com.

Space Complexity Explained — Why Memory Matters in DSA

Fast code that crashes on 1 million inputs because it’s eating all the RAM is still broken code. That’s space complexity.

⚡ What is Space Complexity?

It measures how much memory an algorithm uses relative to input size — expressed in Big-O notation.

📦 Auxiliary Space (What We Measure)

The extra memory beyond the input — variables, arrays, recursion stack.

📊 Common Space Complexities

O(1) — Constant Space

function sum(arr) {
  let total = 0; // 1 variable, regardless of arr size
  for (let x of arr) total += x;
  return total;
}

O(n) — Linear Space

function copyArray(arr) {
  return [...arr]; // New array grows with input
}

O(n) — Recursion Stack

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1); // n frames on call stack
}

⚖️ Time vs Space Trade-off

Approach Time Space
Caching results O(1) lookup O(n) storage
Recalculating O(n) O(1)
In-place sort Same O(1)
Merge sort O(n log n) O(n) extra

🎯 Interview Tip

Always state both complexities: “O(n) time and O(1) space” — this shows complete thinking.

Part 5 of the Bitveen DSA Series. Originally published at bitveen.com