Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
151755 stories
·
33 followers

💀 RIP Copy-Paste: Google NotebookLM Just Killed Manual Data Entry

1 Share

If you’ve ever spent your weekend manually copying rows from a PDF into a spreadsheet, your life is about to get significantly better. Here is why the new NotebookLM update is the biggest productivity unlock of 2025.

Let’s be honest: "AI that writes poetry" is cool, but "AI that does my boring grunt work" is what we actually want.

For the last year, Google NotebookLM has been the sleeper hit of the AI world. It’s not just a chatbot; it’s a "grounded" research assistant that actually cites its sources. But until yesterday, it had one major flaw: it was great at reading your documents, but terrible at structuring them.

That changed this week.

Google just dropped Structured Data Tables for NotebookLM, and it is effectively a "Delete" button for the worst part of data extraction.

🤯 The Problem: "Unstructured Data Hell"

We all have that one folder.

  • The folder full of client meeting transcripts.
  • The folder with 50 different PDF invoices.
  • The "Competitor Analysis" folder with 12 different messy web scrapes.

Previously, if you wanted to compare pricing across those 12 competitors, you had to open each file, find the number, and type it into Excel. It’s the kind of soul-crushing work that makes you question your career choices.

🛠️ The Fix: One Prompt to Rule Them All

With the new update, NotebookLM can now scan all your uploaded sources simultaneously and synthesize them into a clean, structured table.

Here is the workflow that is going viral:

  1. Upload the Mess: Drag and drop 20 distinct PDF reports into a notebook.
  2. The Magic Prompt: Ask for exactly what you need.

    "Create a table comparing the Q4 revenue, net profit, and primary risk factors for all companies listed in these documents."

  3. One-Click Export: The AI builds the table in seconds. You click "Export to Sheets," and you’re done.

Why Developers Are Freaking Out

It’s not just for business analysts. For devs, this is a massive hack for:

  • Log Analysis: Upload raw logs and ask for a table of "Error Type," "Frequency," and "Timestamp."
  • Documentation audits: Upload your entire repo's README.md files and ask for a table of "Features," "Deprecation Warnings," and "Version Compatibility."
  • Resume Screening: Upload 50 resumes and get a table of "Years of Experience," "Python Proficiency," and "Github Link."

🚀 How to Use It (Step-by-Step)

If you want to try this right now, here is the cheat sheet:

Step 1: The Setup
Go to NotebookLM and create a new notebook. Upload your source documents. (Pro Tip: It handles PDFs, Google Docs, Slides, and even copied text).

Step 2: The Command
Don't be vague. The "Data Table" feature works best with specific column requests.

  • ❌ Bad Prompt: "Make a table."
  • ✅ Good Prompt: "Create a table with columns for Feature Name, Release Date, and API Endpoint based on these patch notes."

Step 3: The Refinement
The AI might miss a nuanced detail. You can chat with it to refine the table before you export.

"Add a column for 'Severity Level' based on the bug reports."

Step 4: The Escape Hatch
Once it looks good, hit the Export to Google Sheets button. Now you have a native spreadsheet you can filter, sort, and graph.

🔮 The Bigger Picture: "Progressive Tabular Synthesis"

There is a concept floating around the AI engineering space called Progressive Tabular Synthesis. It’s the idea that you don't just extract data; you use the structure to find new insights.

By forcing unstructured text (like a messy transcript) into a strict table format (Rows & Columns), you force the model to hallucinate less and categorize more rigorously. You are effectively turning "vibes" into "database entries."

🏁 Final Verdict

We are moving from the "Chatbot Era" to the "Agentic Era." We don't just want to talk to AI; we want it to do things.

Converting raw information into structured data is the first step in automating entire workflows. Today, it's a table in Google Sheets. Tomorrow, it's an API call that triggers a deployment.

Stop copy-pasting. Let the machine do it.

🗣️ Discussion

Have you tried the new NotebookLM features yet? What's the most boring data entry task you're planning to automate with this? Drop a comment below! 👇

Read the whole story
alvinashcraft
17 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

CSS :has() Selector Deep Dive: The Parent Selector That Changes Everything

1 Share

For over two decades, web developers have asked for one thing: a parent selector in CSS. The ability to style an element based on its children seemed like an impossible dream. JavaScript was our only escape hatch, leading to countless workarounds, state management headaches, and performance compromises.

Then came :has().

The CSS :has() selector fundamentally changes how we think about styling web interfaces. It's not just a parent selector—it's a relational pseudo-class that lets you select elements based on what they contain, what comes after them, or virtually any relationship you can express in CSS.

In this comprehensive guide, we'll explore everything you need to know about :has(): from basic syntax to advanced patterns, performance considerations, and real-world applications that will transform your CSS architecture.

The Long Wait for a Parent Selector

Before diving into :has(), let's understand why this feature took so long to arrive and why it matters so much.

The Historical Problem

Traditional CSS selectors work in one direction: from parent to child. You can style a child based on its parent, but never the reverse.

/* This works - styling children based on parent */
.card .title {
  font-size: 1.5rem;
}

/* This was impossible - styling parent based on child */
/* .card:contains(.featured-badge) { ... } ← NOT VALID CSS (before :has()) */

This limitation forced developers into awkward patterns:

  1. Adding modifier classes: .card.has-featured-badge { ... }
  2. JavaScript-based styling: Listening for DOM changes and toggling classes
  3. Restructuring HTML: Moving elements around to fit CSS limitations

Each approach had drawbacks. Modifier classes created coupling between logic and styles. JavaScript solutions added complexity and potential performance issues. HTML restructuring compromised semantic markup.

Why It Took So Long

Browser vendors hesitated to implement parent selectors due to performance concerns. CSS selectors are evaluated right-to-left for efficiency. A parent selector could theoretically require the browser to:

  1. Find all matching descendant elements
  2. Walk up the DOM tree for each match
  3. Apply styles to ancestors

This approach could cause significant rendering bottlenecks on complex pages. The :has() specification addresses these concerns with specific parsing and evaluation rules, making it performant enough for real-world use.

Understanding :has() Fundamentals

The :has() pseudo-class accepts a relative selector list as its argument and matches elements that have at least one descendant matching that selector.

Basic Syntax

parent:has(child-selector) {
  /* styles applied to parent */
}

The element before :has() is styled when it contains any element matching the selector inside the parentheses.

Your First :has() Example

Let's start with a practical example. Consider a card component:

<div class="card">
  <img src="product.jpg" alt="Product">
  <h3>Product Title</h3>
  <p>Product description...</p>
</div>

<div class="card">
  <h3>Text-Only Card</h3>
  <p>This card has no image.</p>
</div>

Previously, styling cards differently based on whether they contain an image required JavaScript or separate CSS classes. With :has():

/* Default card styles */
.card {
  padding: 1rem;
  border-radius: 8px;
  background: white;
}

/* Cards WITH images get a different layout */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 1rem;
}

/* Cards WITHOUT images center their content */
.card:not(:has(img)) {
  text-align: center;
  max-width: 400px;
}

This is pure CSS. No JavaScript. No extra classes. The styling adapts automatically based on content.

Beyond Parent Selection: :has() as a Relational Selector

While "parent selector" captures the most common use case, :has() is far more powerful. It's a relational pseudo-class that can express complex element relationships.

Sibling Selection with :has()

You can select elements based on their siblings using :has() with sibling combinators:

/* Select a label that has a required input after it */
label:has(+ input:required) {
  font-weight: bold;
}

label:has(+ input:required)::after {
  content: " *";
  color: #e74c3c;
}

This pattern is incredibly useful for form styling without JavaScript.

Ancestor Selection

:has() can look multiple levels deep:

/* Style a section if it contains ANY error anywhere inside */
.form-section:has(.error-message) {
  border-left: 4px solid #e74c3c;
  background: #fdf2f2;
}

/* Style a table row if it contains an editable cell */
tr:has(td[contenteditable="true"]) {
  background: #fffef0;
}

Combining Multiple Conditions

You can chain multiple :has() conditions:

/* Card with both an image AND a featured badge */
.card:has(img):has(.featured-badge) {
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  border: 2px solid gold;
}

Or use a selector list within :has():

/* Card with either a video OR an image */
.card:has(img, video) {
  min-height: 300px;
}

Real-World Use Cases and Patterns

Let's explore practical applications that showcase the true power of :has().

1. Form Validation Styling

One of the most impactful uses of :has() is CSS-only form validation feedback:

/* Style form group based on input validity */
.form-group:has(input:invalid:not(:placeholder-shown)) {
  --input-border-color: #e74c3c;
  --input-bg: #fdf2f2;
}

.form-group:has(input:valid:not(:placeholder-shown)) {
  --input-border-color: #27ae60;
  --input-bg: #f0fdf4;
}

.form-group input {
  border: 2px solid var(--input-border-color, #ddd);
  background: var(--input-bg, white);
  transition: all 0.2s ease;
}

/* Show validation message only when invalid */
.form-group .error-message {
  display: none;
  color: #e74c3c;
  font-size: 0.875rem;
  margin-top: 0.5rem;
}

.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
  display: block;
}

This creates a complete, reactive validation UI without a single line of JavaScript.

2. Navigation Active States

Style navigation items based on the current page or active state:

/* Highlight dropdown parent when any child link is active */
.nav-dropdown:has(.nav-link.active) > .dropdown-toggle {
  color: var(--primary-color);
  font-weight: bold;
}

/* Show dropdown indicator when it has submenu items */
.nav-item:has(.submenu)::after {
  content: "▼";
  font-size: 0.75em;
  margin-left: 0.5rem;
}

3. Empty State Handling

Detect and style empty containers:

/* Style container when it has no items */
.item-grid:not(:has(.item)) {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 300px;
}

.item-grid:not(:has(.item))::before {
  content: "No items to display";
  color: #999;
  font-style: italic;
}

4. Quantity Queries

This is where :has() gets creative. You can style elements based on sibling count:

/* Style differently when there's only one item */
.item:only-child {
  width: 100%;
}

/* When there are exactly two items */
.item:first-child:nth-last-child(2),
.item:last-child:nth-last-child(2) {
  width: 50%;
}

/* Style the container when it has more than 3 items */
.item-container:has(.item:nth-child(4)) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

5. Focus Management

Implement advanced focus states for accessibility:

/* Highlight entire card when any focusable element inside is focused */
.card:has(:focus-visible) {
  outline: 3px solid var(--focus-color);
  outline-offset: 2px;
}

/* Dim other cards when one is focused (for keyboard navigation) */
.card-container:has(.card:focus-visible) .card:not(:focus-visible):not(:has(:focus-visible)) {
  opacity: 0.7;
}

6. Responsive Component Behavior

Create components that adapt based on their content:

/* Switch header layout based on content */
.header:has(.search-bar):has(.user-menu) {
  display: grid;
  grid-template-columns: auto 1fr auto;
}

.header:has(.search-bar):not(:has(.user-menu)) {
  display: grid;
  grid-template-columns: auto 1fr;
}

/* Adjust sidebar width when expanded sections exist */
.sidebar:has(.section.expanded) {
  width: 320px;
}

.sidebar:not(:has(.section.expanded)) {
  width: 240px;
}

7. Table Enhancements

Style tables dynamically based on content:

/* Highlight row when checkbox is checked */
tr:has(input[type="checkbox"]:checked) {
  background: #e3f2fd;
}

/* Style header differently when table has sortable columns */
table:has(th[data-sortable]) thead {
  cursor: pointer;
}

table:has(th[data-sortable]) th:hover {
  background: #f5f5f5;
}

/* Add visual indicator when table is empty */
table:not(:has(tbody tr)) {
  position: relative;
}

table:not(:has(tbody tr))::after {
  content: "No data available";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #999;
}

Advanced Patterns and Techniques

Combining :has() with :not()

The combination of :has() and :not() unlocks powerful negation patterns:

/* Style elements that DON'T contain something */
.container:not(:has(.advertisement)) {
  /* Premium, ad-free experience styles */
  padding: 2rem;
}

/* Cards without media */
.card:not(:has(img, video, iframe)) {
  /* Text-only card styles */
  font-size: 1.1rem;
  line-height: 1.8;
}

Debugging with :has()

Use :has() for development-time debugging:

/* Highlight images without alt text during development */
img:not([alt]),
img[alt=""] {
  outline: 5px solid red !important;
}

/* Highlight forms with empty actions */
form:not([action]),
form[action=""] {
  outline: 3px dashed orange !important;
}

/* Find links that open in new tabs without rel="noopener" */
a[target="_blank"]:not([rel*="noopener"]) {
  outline: 3px solid purple !important;
}

The Forward-Looking Combinator Pattern

Use :has() with the next-sibling combinator for future element selection:

/* Style an element based on what comes AFTER it */
h2:has(+ .special-content) {
  /* This h2 is followed by special content */
  color: var(--accent-color);
  border-bottom: 3px solid currentColor;
}

/* Create a "previous sibling" selector effect */
.item:has(+ .item.active) {
  /* Style the item BEFORE the active one */
  border-right: none;
}

Performance Considerations

While modern browsers handle :has() efficiently, understanding performance implications helps you write optimized CSS.

How Browsers Evaluate :has()

Browsers implement :has() with specific optimizations:

  1. Invalidation limits: Browsers set limits on how deep :has() will search
  2. Subject-based caching: Once matched, results are cached for the subject element
  3. Mutation batching: DOM changes are batched before re-evaluation

Best Practices for Performance

1. Be Specific with Selectors

/* ❌ Broad selector - evaluates for all divs */
div:has(img) { ... }

/* ✅ Specific selector - limits scope */
.card:has(img) { ... }

2. Avoid Deep Nesting

/* ❌ Deep nesting requires traversing entire subtree */
.page:has(.section .container .row .col .card .badge) { ... }

/* ✅ Direct or shallow relationships */
.card:has(> .badge) { ... }

3. Use Direct Child Combinator When Possible

/* ❌ Searches all descendants */
.menu:has(.active) { ... }

/* ✅ Only checks direct children */
.menu:has(> .menu-item.active) { ... }

4. Limit Selector List Length

/* ❌ Long selector lists in :has() */
.container:has(img, video, audio, iframe, canvas, svg, object, embed) { ... }

/* ✅ Semantic grouping with data attributes */
.container:has([data-media]) { ... }

Measuring Performance Impact

To measure :has() performance:

// Use the Performance API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'style-recalc') {
      console.log(`Style recalculation: ${entry.duration}ms`);
    }
  }
});

observer.observe({ entryTypes: ['measure'] });

Or use browser DevTools:

  1. Open Performance tab
  2. Record while interacting with elements that trigger :has() re-evaluation
  3. Look for "Recalculate Style" entries

Browser Support and Fallbacks

As of late 2024, :has() enjoys excellent browser support:

  • Chrome: 105+ (August 2022)
  • Edge: 105+ (August 2022)
  • Safari: 15.4+ (March 2022)
  • Firefox: 121+ (December 2023)
  • Opera: 91+

This means over 95% of global users can use :has() today.

Creating Fallbacks

For legacy browser support, use feature queries:

/* Default fallback styles */
.card {
  display: block;
}

/* Progressive enhancement with :has() */
@supports selector(:has(*)) {
  .card:has(img) {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

The @supports Pattern

A robust pattern for :has() fallbacks:

/* Base styles - work everywhere */
.form-group {
  margin-bottom: 1rem;
}

.form-group.has-error {
  border-color: red;
}

/* Modern browsers with :has() support */
@supports selector(:has(*)) {
  /* Remove JavaScript-dependent class usage */
  .form-group.has-error {
    border-color: initial;
  }

  /* Use :has() instead */
  .form-group:has(input:invalid:not(:placeholder-shown)) {
    border-color: red;
  }
}

Common Mistakes and How to Avoid Them

Mistake 1: Forgetting :has() Specificity

:has() doesn't add to specificity based on its contents:

/* These have the same specificity! */
.card { ... }           /* (0, 1, 0) */
.card:has(img) { ... }  /* (0, 1, 0) */

The fix:

.card:has(img) {
  /* Use custom properties or increasing selector specificity */
}

/* Or be explicit: */
.card.with-media:has(img) { ... }  /* (0, 2, 0) */

Mistake 2: Over-Reliance on :has()

Not everything needs :has(). Simpler solutions often exist:

/* ❌ Overcomplicating */
.btn:has(svg):has(span) { ... }

/* ✅ Simpler with a utility class */
.btn.icon-button { ... }

Mistake 3: Ignoring Selector Performance

/* ❌ Expensive - checks every element */
*:has(.some-class) { ... }

/* ✅ Scoped appropriately */
.component:has(.some-class) { ... }

Mistake 4: Circular Dependencies

Some selectors can create infinite loops:

/* ⚠️ Be careful with self-referential patterns */
.item:has(+ .item:has(+ .item)) { ... }

The Future of CSS Selection

The :has() selector represents a fundamental shift in CSS capabilities, but it's just the beginning. Future CSS specifications are exploring:

  1. CSS Nesting: Now available, works beautifully with :has()
  2. Scroll-driven animations: Combine with :has() for complex scroll effects
  3. Container queries: Pair with :has() for truly responsive components
  4. CSS Mixins: Reusable style patterns that could leverage :has()

:has() with CSS Nesting

Modern CSS nesting makes :has() even more powerful:

.card {
  padding: 1rem;

  &:has(img) {
    display: grid;
    grid-template-columns: 200px 1fr;

    & img {
      border-radius: 8px;
    }
  }

  &:has(.badge) {
    position: relative;

    & .badge {
      position: absolute;
      top: -10px;
      right: -10px;
    }
  }
}

:has() with Container Queries

The ultimate in component-based styling:

.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card:has(img) {
    grid-template-columns: 250px 1fr;
  }
}

@container card (max-width: 399px) {
  .card:has(img) {
    grid-template-columns: 1fr;
  }
}

Practical Refactoring: Before and After

Let's see a real-world refactoring example. Consider a navigation component:

Before :has() (JavaScript Required)

<nav class="main-nav">
  <ul class="nav-list">
    <li class="nav-item has-submenu expanded">
      <a href="#">Products</a>
      <ul class="submenu">
        <li><a href="#" class="active">Software</a></li>
        <li><a href="#">Hardware</a></li>
      </ul>
    </li>
  </ul>
</nav>
// JavaScript required to manage these classes
document.querySelectorAll('.nav-item').forEach(item => {
  if (item.querySelector('.submenu')) {
    item.classList.add('has-submenu');
  }
  if (item.querySelector('.active')) {
    item.classList.add('expanded');
  }
});
.nav-item.has-submenu > a::after {
  content: "▼";
}

.nav-item.expanded > .submenu {
  display: block;
}

After :has() (Pure CSS)

<nav class="main-nav">
  <ul class="nav-list">
    <li class="nav-item">
      <a href="#">Products</a>
      <ul class="submenu">
        <li><a href="#" class="active">Software</a></li>
        <li><a href="#">Hardware</a></li>
      </ul>
    </li>
  </ul>
</nav>
/* No JavaScript needed! */
.nav-item:has(.submenu) > a::after {
  content: "▼";
}

.nav-item:has(.active) > .submenu {
  display: block;
}

.nav-item:has(.active) > a {
  font-weight: bold;
  color: var(--primary);
}

The benefits:

  • Cleaner HTML: No state classes
  • No JavaScript: Removes a runtime dependency
  • Automatic updates: Changes to DOM auto-update styles
  • Better performance: CSS is faster than JavaScript for this

Conclusion

The CSS :has() selector is more than a parent selector—it's a paradigm shift in how we can express styling logic. After two decades of workarounds, we finally have a native way to select elements based on their relationships with other elements.

Key Takeaways

  1. :has() is a relational pseudo-class, not just a parent selector. It can express sibling relationships, descendant conditions, and complex logical combinations.

  2. Browser support is excellent at 95%+. You can use :has() today with simple fallbacks for legacy browsers.

  3. Performance is generally not a concern for well-scoped selectors. Be specific, avoid deep nesting, and use direct child combinators when possible.

  4. :has() reduces JavaScript dependency. Many interactive patterns that required JavaScript can now be pure CSS.

  5. Combine with modern CSS features like nesting and container queries for even more powerful component-based styling.

The web platform continues to evolve, closing gaps that once required JavaScript workarounds. The :has() selector is one of the most significant additions to CSS in years, and mastering it will make you a more effective frontend developer.

Start small—find one JavaScript-dependent style pattern in your codebase and see if :has() can simplify it. You'll be surprised how often the answer is yes.

💡 Note: This article was originally published on the Pockit Blog.

Check out Pockit.tools for 50+ free developer utilities (JSON Formatter, Diff Checker, etc.) that run 100% locally in your browser.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Coding Rust with Claude Code and Codex

1 Share

For a while now, I've been experimenting with AI coding tools and there's something fascinating happening when you combine Rust with agents such as Claude Code or OpenAI's Codex: The experience is fundamentally different from working with Python or JavaScript - and I think it comes down to one simple fact: Rust's compiler acts as an automatic expert reviewer for each edit the AI makes.

If it compiles, it probably works; that's not just a Rust motto: it's becoming the foundation for reliable AI-assisted development.

The problem with AI coding in dynamic languages

When you let Claude Code or Codex loose on a Python codebase, you essentially trust the AI to get things right on its own: Sure, you have linters and type hints (if you are lucky), but there is no strict enforcement: the AI can generate code that looks reasonable, passes your quick review, and then blows up in production because of some edge case nobody thought about.

With Rust, the compiler catch these issues before anything runs. Memory safety incidents? Caught. Data runs? Caught. Lifetime issues? You guessed it—caught in compiler time. This creates a remarkably tight feedback loop that AI coding tools can actually learn from in real time.

Rust's compiler is basically a senior engineer

Here is what makes Rust special for AI coding: the compiler doesn't just say "Error" and leave you guessing: It tells you exactly what went wrong, where it went wrong and often suggests how to fix it; this is absolute gold for AI tools like Codex or Claude Code.

Let me show you what I mean: say the AI writes this code:

fn get_first_word(s: String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

The Rust compiler doesn't fail just with a cryptic message, but it gives you:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:36
  |
1 | fn get_first_word(s: String) -> &str {
  |                   -             ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, 
          but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
1 | fn get_first_word(s: String) -> &'static str {
  |                                 ~~~~~~~~

Look at this. The compiler is literally explaining the ownership model to AI - it is saying to - "Hey, you're trying to return a reference but the thing you're referencing will be dropped when this function ends - that's not going to work."

For an AI coding tool, this is structured, deterministic feedback. The error code E0106 is consistent; the location is found to the exact character; the explanation is clear; and there's even a suggested fix (though in this case the real fix is to change the function signature to borrow instead of taking ownership).

Here's another example that constantly happens when AI tools write concurrent code:

use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("{:?}", data);
    });

    handle.join().unwrap();
}

The compiler response:

error[E0373]: closure may outlive the current function, but it borrows `data`
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `data`
7 |         println!("{:?}", data);
  |                          ---- `data` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |     let handle = thread::spawn(|| {
  |                  ^^^^^^^^^^^^^
help: to force the closure to take ownership of `data`, use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

The compiler literally tells the AI: "Add move here, Claude Code or Codex can parse it, apply the fix and move on - no guesswork, no hoping for the best, no Runtime - Data Races that crash your production system at 3 AM.

This is fundamentally different from what occurs in Python or JavaScript: When an AI produces buggy concurrent code in those languages, you might not even know there is a problem until you hit a race condition under specific load conditions; with Rust, the bug never makes it past the compiler.

Why Rust is perfect for unsupervised AI coding

I came across an interesting observation from Julian Schrittwieser at Anthropic, who put it perfectly:

Rust is great for Claude Code to work unsupervised on larger tasks. The combination of a powerful type system with strong security checks acts like an expert code reviewer, automatically rejecting incorrect edits and preventing bugs.

This matches our experience at Sayna where we built our entire voice processing infrastructure in Rust. When Claude Code or any AI tool changes, the compiler immediately tells it what went wrong, there are no waiting for runtime errors, no debugging sessions to figure out why the audio stream randomly crashes, the errors are clear and actionable

Here's what a typical workflow looks like:

# AI generates code
cargo check

# Compiler output:
error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let r1 = &x;
  |              -- immutable borrow occurs here
4 |     let r2 = &mut x;
  |              ^^^^^^ mutable borrow occurs here
5 |     println!("{}, {}", r1, r2);
  |                        -- immutable borrow later used here

# AI sees this, understands the borrowing conflict, restructures the code
# AI makes changes

cargo check
# No errors, we're good

The beauty here is that every single error has a unique code (E0502 in this case) If you run rustc --explain E0502, you get a full explanation with examples. AI tools can use this to understand not only what went wrong but also why Rust's ownership model prevents this pattern, because the compiler essentially teaches the AI as it codes.

The margin for error becomes extremely small when the compiler provides structured, deterministic feedback that the AI can parse and act on.

Compare this to what you get from a C++ compiler if something goes wrong with templates:

error: no matching function for call to 'std::vector<std::basic_string<char>>::push_back(int)'
   vector<string> v; v.push_back(42);
                      ^

Sure, it tells you that there's a type mismatch, BUT imagine if this error was buried in a 500-line template backtrace and you can find an AI to parse that accurately.

Rust's error messages are designed to be human-readable, which accidentally makes them perfect for AI consumption: each error contains the exact source location with line and column numbers, an explanation of which rule was violated, suggestions for how to fix it (when possible) and links to detailed documentation.

When Claude Code or Codex runs Cargo Check it receives a structured error on which it can directly act. The feedback loop is measured in seconds, not debugging sessions.

Setting up your rust project for AI-coding

One thing that made our development workflow significantly better at Sayna was investing in a correct CLAUDE. md file, which is essentially a guideline document that lives in your repository and gives AI coding tools context about your project structure, conventions and best practices.

Specifically for Rust projects you want to include:

  1. Cargo Workspace Structure - How your crates are organized
  2. Error handling patterns - Do you use anyhow, thiserror or custom error types?
  3. Async Runtime - Are you on tokio, async-std or something else?
  4. Testing conventions - Integration tests location, mocking patterns
  5. Memory management guidelines - When to use Arc, Rc or plain references.

The combination of Rust's strict compiler with well-documented project guidelines creates an environment where AI tools can operate with high confidence; they know the rules and the compiler enforces them.

Real examples from production

At Sayna—WebSocket - handling, audio processing pipelines, real-time STT/TTS - provider abstraction we use Rust for all the heavy lifting These are exactly the kind of systems where memory safety and concurrency guarantee matters

When Claude code refactors our WebSocket message handlers, it can't eat it in an accidental way, when it changes our audio buffer management, it can't create a use-after-free bug because the language simply does not allow it.

// The compiler ensures this audio buffer handling is safe
pub async fn process_audio_chunk(&self, chunk: Bytes) -> Result<()> {
    let processor = self.processor.lock().await;
    processor.feed(chunk)?;

    while let Some(result) = processor.next_result().await {
        self.tx.send(result).await?;
    }

    Ok(())
}

An AI tool might need several iterations to get the borrowing and lifetimes right, BUT each iteration is guided by specific compiler errors: no guessing, no hoping for the best.

Codex Going Rust is Not a Coincidence

OpenAI recently rewrote their codex CLI entirely in Rust, it wasn't just about performance - though that was definitely a factor - they explicitly mentioned that Rust eliminates entire classes of bugs at compile time - if OpenAI is betting on Rust for their own AI - coding infrastructure - it tells you something about where this is headed.

The security implications are also massive, codex now runs in sandboxed environments using Rust safety guarantees combined with OS isolation (Landlock on Linux, Sandbox-exec on macOS), when you have AI-generated code running on your machine, having compile-time security guarantees is not optional.

The learning curve trade-off

I won't pretend that Rust is easy to learn because the ownership model takes time to internalize and lifetimes can be frustrating when you are starting out—AI - Coding tools are actually quite good at dealing with Rust's sharp edges.

My favorite trick is to tell Claude Code to "fix the lifetimes" and let it figure out which combination of and, ref, as_ref() and explicit lifetime annotations make my code compile while I concentrate on the actual logic and architecture.

// Before: Claude fix this
fn process(&self, data: Vec<String>) -> &str {
    &data[0]  // Won't compile - returning reference to local data
}

// After: Claude's solution
fn process(&self, data: &[String]) -> &str {
    &data[0]  // Works - borrowing from input parameter
}

This is actually a better way to learn Rust than struggling alone through compiler errors: you see patterns, you understand why certain approaches work and the AI explains its reasoning when you ask.

Making AI-coding work for your team

If you're considering using Claude Code or Codex for Rust development, here's what I'd recommend:

  1. Invest in your CLAUDE. md - Document your patterns, conventions and architectural decisions The AI will follow them.

  2. Use cargo clippy aggressively - enable all lints. More feedback means better AI output.

  3. CI with strict checks - Make sure that Cargo test, Cargo clippy and Cargo fmt are running on every change; AI tools can verify their work before you even look it up.

  4. Start with well defined tasks - Rust's type system shines when the boundaries are clear: define your traits and types first then let AI implement the logic.

  5. Verify but trust - The compiler catches a lot, BUT not everything: Logic errors still slip through: code review is still essential.

The Future of AI-Assisted Systems Programming

We're at an interesting inflection point: rust is growing quickly in systems programming and AI coding tools are actually becoming useful for production work: the combination creates something more than the sum of its parts.

At Sayna, our voice processing infrastructure handles real-time audio streams, multiple provider integrations and complex state management: all built in Rust, with significant AI assistance: means we can move faster without constantly worry over memory bugs or race conditions.

If you've already tried Rust and found the learning curve too steep, give it another try with Claude Code or Codex as your pair programmer. The experience is different when you have an AI that can navigate ownership and borrowing patterns while you focus on building things.

The tools finally catching up to the promise of the language

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

The Secret Life of Go: Error Handling

1 Share

Chapter 12: The Sound of Breaking Glass

Monday morning arrived with a heavy gray mist clinging to the city. Inside the archive, the silence was absolute, broken only by the rhythmic scrape-scrape-scrape of Eleanor’s bone folder smoothing a crease in an ancient map.

Ethan walked in, his umbrella dripping. He set a small bakery box on the desk. "Pear and almond tart. And a Flat White, extra foam."

Eleanor paused her work. She inspected the tart. "A classic combination. The sweetness of the pear balances the bitterness of the almond. Well chosen."

Ethan sat down, opening his laptop with a sigh that was a little too loud.

"That sound," Eleanor said without looking up. "That is the sigh of a programmer fighting the compiler."

"Not the compiler," Ethan corrected. "The boilerplate. I'm writing this file parser, and half my code is just checking for errors. It feels… primitive. I miss exceptions. I miss try-catch blocks where I can just wrap everything in a bubble and handle problems at the end."

He turned his screen to her.

func ParseFile(filename string) {
    f, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    // ... logic continues
}

"I was thinking," Ethan said. "Maybe I should just panic when things go wrong and recover at the top level? Like a global exception handler?"

Eleanor set down her bone folder. Her expression hardened.

"Ethan, imagine you are reading a book," she began. "You are on page 50. Suddenly, without warning, you are teleported to page 200. You have no idea how you got there or what happened in between. That is an Exception."

She took a sip of the Flat White. "Exceptions are invisible control flow. They allow a function to jump the stack, bypassing return statements and cleanup logic. In Go, we value visibility over convenience. We treat errors as Values."

Errors are just Values

She opened a new file. "An error in Go is not a magical event. It is a value, just like an integer or a string. It is a simple interface:"

type error interface {
    Error() string
}

"It is just anything that can describe itself as a string. Because it is a value, you can pass it, store it, wrap it, or ignore it—though you certainly shouldn't."

She typed a new example. "You said you wanted to panic. Let me show you why treating errors as values is better."

package main

import (
    "errors"
    "fmt"
    "os"
)

// Define a "Sentinel Error" - a constant value we can check against
var ErrEmptyConfig = errors.New("config file is empty")

func loadConfig(path string) (string, error) {
    if path == "" {
        // We create the error value right here
        return "", errors.New("path cannot be empty")
    }

    file, err := os.Open(path)
    if err != nil {
        // We return the error value up the stack
        return "", err
    }
    defer file.Close()

    stat, err := file.Stat()
    if err != nil {
        return "", err
    }

    if stat.Size() == 0 {
        // We return our specific sentinel error
        return "", ErrEmptyConfig
    }

    return "config_data", nil
}

func main() {
    _, err := loadConfig("missing.txt")
    if err != nil {
        fmt.Println("Error occurred:", err)
    }
}

"Look at the flow," Eleanor pointed. "There are no hidden jumps. The error travels up the stack through return values. You can see exactly where it exits. This is Explicit Control Flow."

"But it's so verbose," Ethan grumbled. "if err != nil is everywhere."

"It is repetitive," Eleanor conceded. "But consider the alternative. If file.Stat() threw an exception, would you remember to close the file? In Go, the defer file.Close() runs no matter what errors happen later. The explicit checks force you to decide: 'What do I do if this fails right now?'"

Decorating the Error

"However," Eleanor added, "simply returning err is often lazy. If loadConfig returns 'file not found', the caller doesn't know which file. You need to add context."

She modified the code:

func loadConfig(path string) (string, error) {
    file, err := os.Open(path)
    if err != nil {
        // Wrap the error with context using %w
        // We only wrap when we are adding useful information.
        return "", fmt.Errorf("failed to open config at %s: %w", path, err)
    }
    // ...
}

"The %w verb stands for 'wrap'. It puts the original error inside a new one. It creates a chain."

Ethan frowned. "But if I wrap it, how do I check what the original error was? If I want to check for ErrEmptyConfig?"

"A brilliant question." Eleanor opened her notebook. "Before Go 1.13, this was hard. Now, we have errors.Is and errors.As."

Unwrapping the Mystery

She typed a robust error handling example:

package main

import (
    "errors"
    "fmt"
    "io/fs"
)

func main() {
    _, err := loadConfig("config.json")
    if err != nil {
        // 1. Check if it matches a specific value (Sentinel)
        if errors.Is(err, ErrEmptyConfig) {
            fmt.Println("Please provide a non-empty config file.")
            return
        }

        // 2. Check if it matches a specific TYPE (like PathError)
        var pathErr *fs.PathError
        if errors.As(err, &pathErr) {
            fmt.Println("File system error on path:", pathErr.Path)
            return
        }

        // 3. Fallback
        fmt.Println("Unknown error:", err)
    }
}

"Think of errors.Is like checking equality. It looks through the layers of wrapping to see if ErrEmptyConfig is buried somewhere inside. Think of errors.As like a type assertion. It checks if there is an error of type *fs.PathError inside."

Ethan studied the code. "So I can wrap an error ten times, adding context at every layer, and errors.Is can still find the original cause?"

"Exactly. You preserve the root cause while adding the narrative of how it failed."

Don't Panic

"So when can I use panic?" Ethan asked.

"Almost never," Eleanor replied sternly. "Panic is for when the program is fundamentally broken and cannot continue—like running out of memory, or a developer mistake that makes the internal state invalid. It is not for 'file not found' or 'network timeout'."

She picked up the tart. "If you panic in a library I import, you crash my entire application. That is rude. Return an error. Let me decide if I want to crash."

Ethan watched the rain streak against the window. "It forces you to handle the unhappy path first."

"Yes. It aligns your code to the left," Eleanor said, using her hand to trace the shape of the code in the air. "The 'happy path'—the successful logic—stays minimally indented on the left side of the screen. The error handling nests to the right and returns early. It makes the logic easy to scan."

Eleanor took a bite of the tart. "An error is not an exception, Ethan. An exception says 'something unexpected happened.' An error says 'this is a possible outcome of this function.' And in software, failure is always a possible outcome."

She wiped a crumb from her lip. "Treat your errors with respect. Give them context. Check them specifically. And never, ever assume they won't happen."

Ethan closed his laptop lid. "No more try-catch."

"No more try-catch," Eleanor agreed. "Just values. Passed from hand to hand, until they are resolved."

Key Concepts from Chapter 12

Errors are Values: In Go, errors are not special control flow mechanisms. They are values implementing the error interface.

The error Interface:

type error interface {
    Error() string
}

Sentinel Errors: Pre-defined global variables for specific errors (e.g., io.EOF, errors.New("some error")). Useful for simple equality checks.

Wrapping Errors: Using fmt.Errorf("... %w ...", err) wraps an error, adding context while preserving the original error underneath.

errors.Is: Checks if a specific error value exists anywhere in the wrapping chain. Use this instead of == when handling wrapped errors.

  • Example: if errors.Is(err, io.EOF)

errors.As: Checks if an error of a specific type exists anywhere in the chain, and assigns it to a variable.

var pathErr *fs.PathError // Note: must be a pointer to the error type
if errors.As(err, &pathErr) {
    fmt.Println(pathErr.Path)
}

Panic vs. Error:

  • Error: Expected failure modes (I/O, validation, network). Handled by returning values.
  • Panic: Unexpected, unrecoverable state (nil pointer, index out of range). Crashes the program (unless recovered, which is rare).
  • Rule: Don't panic in libraries. Return errors.

Align the Happy Path: Structure code to handle errors early and return. Keep the successful logic minimally indented.

Next chapter: Testing—where Eleanor shows Ethan that writing tests isn't a chore, but the only way to prove his system actually works.

Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Read the whole story
alvinashcraft
8 hours ago
reply
Pennsylvania, USA
Share this story
Delete

💡 How to Install Gemini CLI on Your Computer

1 Share

Gemini CLI is a great open source command line tool that lets you use Google's Gemini on your computer right in the terminal. You can install and use it on Windows, Mac, or Linux. It can help you with code development, file and project management, and AI-powered assistance. Why I like and use it is also not only because it is a very powerful tool, but also because of its cost - or in reality - no cost! For individuals, it has a 1000 requests per day limit, which is way too much for the average user (60 requests per minute). So, try it, see if you like it, and comment under the article. Now, let's install it:

🛠 Prerequisites

  • Node.js (v20 or higher) — this provides the runtime environment Gemini CLI needs.
  • A working terminal.

🚀 Step 1 — Install Gemini CLI

npm install -g @google/gemini-cli

▶️ Step 2 — Launch Gemini CLI

In your command-line shell type:

gemini

🔑 Step 3 — Authenticate

You will need to log in with your Google account - that is how Google will track how many requests you make. But don't worry, they are giving more than needed for an average user.

🧪 Step 4 — Try a Command!

Enter a prompt and wait for an answer. My question about why the sky is blue—it took 9 seconds to answer :)

About the Author

Deividas Strole is a Full-Stack Developer based in California, specializing in Java, Spring Boot, React, and AI-driven development. He writes about software engineering, modern full-stack development, and digital marketing strategies.

Connect with me:

Read the whole story
alvinashcraft
8 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Control Your Space or You Lose Your Mind Working Remote

1 Share

Remote work is sold as freedom. Work from anywhere. Laptop by the pool. Coffee shop vibes. Total flexibility.

That story sounds good, but it is mostly nonsense.

Ashkan Rajaee argues something uncomfortable that most remote founders do not want to hear. If you do not aggressively control your environment, remote work will eventually break you and stall your business.

This idea goes against popular remote culture, and that is exactly why it matters.

The Hidden Ceiling Nobody Talks About

Most people think remote failure comes from lack of discipline, motivation, or talent. That assumption is flawed.

The real bottleneck is environment.

According to :contentReference[oaicite:0]{index=0}, every remote operator eventually hits a ceiling if their workspace is chaotic, shared, or constantly shifting. You might survive short term. You might even grow a bit. But at scale, cracks turn into collapse.

The logic is simple. If you do not control your space, something else does. Family. Noise. Bad internet. Missing gear. All of it drains cognitive energy that should be spent building.

This is not about comfort. It is about leverage.

One Space. Total Control.

Rajaee’s core rule is brutal and clear. You need one place that you fully control.

Not five locations. Not a backpack setup. One environment where noise, internet, and equipment are predictable every single day.

That might sound extreme, but consider the alternative. Constant setup and teardown. Forgotten chargers. Spotty WiFi. Interrupted calls. Each issue feels small, but over a year, the damage compounds.

You cannot scale a serious operation while mentally tracking cables and battery percentages.

Noise Is the First Enemy

Noise is not just sound. It is interruption.

If anyone can walk into your workspace while you are working, you are not in control. Period.

This includes well meaning partners, kids, roommates, or anyone else sharing your space. Even quick questions fracture focus. Deep work does not survive open doors.

Rajaee recommends a dedicated office, even a small one person room. A Regus office near home works. So does a properly isolated home office. What matters is the boundary.

No access. No interruptions. No negotiation.

This is not antisocial. It is professional.

Internet Is Not the Place to Save Money

Residential internet is cheaper. It is also unreliable.

Commercial internet costs more for a reason. Service level agreements. Dedicated support. No throttling. Faster fixes when something breaks.

Dropped calls are not just annoying. They erode trust with clients, partners, and teams. Multiply those drops across hundreds of calls per year and the cost becomes obvious.

If your income depends on being online, your internet is infrastructure, not a utility.

Gear Multiplies Output or Destroys It

Minimalism is trendy. It is also inefficient at scale.

Rajaee is known for running extreme multi monitor setups, sometimes up to seven screens. That might sound excessive, but the reasoning is solid. Every window switch burns mental energy.

Gestures. Swipes. Tab juggling. Those are micro costs that add up fast.

Multiple monitors externalize information. Calendars stay visible. Dashboards stay open. Decisions get faster.

The same applies to peripherals. Dedicated microphones. Cameras. Lighting. Backup devices. Redundancy is not luxury. It is insurance against friction.

Why Laptop Nomad Culture Fails

Working from anywhere sounds empowering until you track the hidden costs.

Every pack and unpack shifts focus away from strategy and execution. Instead of thinking about growth, you think about adapters, batteries, and cables.

Rajaee calls this the biggest mistake remote workers make. Mobility becomes a distraction disguised as freedom.

If you must travel, fine. But your core workspace should always be waiting for you, powered on, configured, and ready.

No friction. No setup rituals. No excuses.

Why Companies Force Offices

Here is the uncomfortable truth.

Companies want employees in offices because offices work.

Not because of control for its own sake, but because structured environments eliminate chaos. Internet is there. Gear is there. The workspace is ready every day.

That structure produces efficiency. Efficiency produces profit.

Remote founders who ignore this lesson try to run companies without the very systems businesses have relied on for decades.

The Non Negotiable Rule

If you want to build a serious company remotely, control your space or accept the ceiling.

Noise. Internet. Gear.

Ignore any one of them and growth becomes fragile.

Remote work is not about being anywhere. It is about being effective.

And effectiveness starts with an environment that serves the work instead of sabotaging it.

Read the whole story
alvinashcraft
9 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories